@@ -2,12 +2,38 @@ package runtime
22
33import (
44 "context"
5+ "os"
56 "sort"
7+ "strings"
8+ "time"
69
710 "github.com/GrayCodeAI/eyrie/catalog"
811 "github.com/GrayCodeAI/eyrie/config"
12+ "github.com/GrayCodeAI/eyrie/credentials"
913)
1014
15+ var chatProviderPreferenceOrder = []string {
16+ "openai" ,
17+ "anthropic" ,
18+ "openrouter" ,
19+ "grok" ,
20+ "gemini" ,
21+ "vertex" ,
22+ "bedrock" ,
23+ "zai_coding" ,
24+ "zai_payg" ,
25+ "canopywave" ,
26+ "deepseek" ,
27+ "azure" ,
28+ "opencodego" ,
29+ "kimi" ,
30+ "xiaomi_mimo_payg" ,
31+ "xiaomi_mimo_token_plan" ,
32+ "minimax_token_plan" ,
33+ "minimax_payg" ,
34+ "ollama" ,
35+ }
36+
1137// DefaultModelProviderFilter returns the catalog provider id to use when listing models
1238// with no explicit provider (e.g. /config model picker after paste-key).
1339// Order: provider.json default → first configured deployment (stable sort by id).
@@ -35,3 +61,143 @@ func DefaultModelProviderFilter(ctx context.Context) string {
3561 }
3662 return ""
3763}
64+
65+ // PreferredProvider returns the runtime-owned provider choice when a host has
66+ // not pinned one explicitly. Active selection wins first, then inferred model
67+ // ownership, then configured providers ordered by runtime preference, and
68+ // finally credential detection as a last resort.
69+ func PreferredProvider (ctx context.Context ) string {
70+ if ctx == nil {
71+ ctx = context .Background ()
72+ }
73+ if provider := normalizeRuntimeProviderID (ActiveProvider (ctx )); provider != "" && providerConfigured (ctx , provider ) {
74+ return provider
75+ }
76+ if model := ActiveModel (ctx ); model != "" {
77+ if provider := inferProviderForModel (ctx , model ); provider != "" && providerConfigured (ctx , provider ) {
78+ return provider
79+ }
80+ }
81+ if provider := preferredConfiguredProvider (ctx ); provider != "" {
82+ return provider
83+ }
84+ return preferredDetectedProvider ()
85+ }
86+
87+ func preferredConfiguredProvider (ctx context.Context ) string {
88+ rt , err := Load (ctx )
89+ if err != nil || rt == nil {
90+ return ""
91+ }
92+ rows , err := rt .DeploymentRows ()
93+ if err != nil || len (rows ) == 0 {
94+ return ""
95+ }
96+ configured := make (map [string ]struct {}, len (rows ))
97+ for _ , row := range rows {
98+ if ! row .Configured {
99+ continue
100+ }
101+ if provider := catalog .CanonicalProviderID (row .ProviderID ); provider != "" {
102+ configured [provider ] = struct {}{}
103+ }
104+ }
105+ for _ , provider := range chatProviderPreferenceOrder {
106+ if _ , ok := configured [provider ]; ok {
107+ return provider
108+ }
109+ }
110+
111+ ordered := make ([]string , 0 , len (configured ))
112+ for provider := range configured {
113+ ordered = append (ordered , provider )
114+ }
115+ sort .Strings (ordered )
116+ if len (ordered ) == 0 {
117+ return ""
118+ }
119+ return ordered [0 ]
120+ }
121+
122+ func preferredDetectedProvider () string {
123+ for _ , provider := range chatProviderPreferenceOrder {
124+ switch provider {
125+ case "ollama" :
126+ if runtimeEnvValue ("OLLAMA_BASE_URL" ) != "" {
127+ return provider
128+ }
129+ default :
130+ profile , ok := runtimeProfileForProvider (provider )
131+ if ! ok {
132+ continue
133+ }
134+ ready := true
135+ for _ , envKey := range profile .DetectionEnv {
136+ if runtimeEnvValue (envKey ) == "" {
137+ ready = false
138+ break
139+ }
140+ }
141+ if ready {
142+ return provider
143+ }
144+ }
145+ }
146+ return ""
147+ }
148+
149+ func runtimeProfileForProvider (provider string ) (config.RuntimeProviderProfile , bool ) {
150+ switch provider {
151+ case "anthropic" :
152+ return config .AnthropicRuntimeProfile , true
153+ case "openai" :
154+ return config .OpenAIRuntimeProfile , true
155+ case "openrouter" :
156+ return config .OpenRouterRuntimeProfile , true
157+ case "grok" :
158+ return config .GrokRuntimeProfile , true
159+ case "gemini" :
160+ return config .GeminiRuntimeProfile , true
161+ case "vertex" :
162+ return config .VertexRuntimeProfile , true
163+ case "bedrock" :
164+ return config .BedrockRuntimeProfile , true
165+ case "zai_coding" :
166+ return config .ZAICodingRuntimeProfile , true
167+ case "zai_payg" :
168+ return config .ZAIPaygRuntimeProfile , true
169+ case "canopywave" :
170+ return config .CanopyWaveRuntimeProfile , true
171+ case "deepseek" :
172+ return config .DeepSeekRuntimeProfile , true
173+ case "azure" :
174+ return config .AzureRuntimeProfile , true
175+ case "opencodego" :
176+ return config .OpenCodeGoRuntimeProfile , true
177+ case "kimi" :
178+ return config .KimiRuntimeProfile , true
179+ case "xiaomi_mimo_payg" :
180+ return config .XiaomiPaygRuntimeProfile , true
181+ case "xiaomi_mimo_token_plan" :
182+ return config .XiaomiTokenPlanRuntimeProfile , true
183+ case "minimax_token_plan" :
184+ return config .MiniMaxTokenPlanRuntimeProfile , true
185+ case "minimax_payg" :
186+ return config .MiniMaxPaygRuntimeProfile , true
187+ default :
188+ return config.RuntimeProviderProfile {}, false
189+ }
190+ }
191+
192+ func runtimeEnvValue (key string ) string {
193+ key = strings .TrimSpace (key )
194+ if key == "" {
195+ return ""
196+ }
197+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
198+ defer cancel ()
199+ if value := credentials .LookupSecret (ctx , key ); value != "" {
200+ return value
201+ }
202+ return strings .TrimSpace (os .Getenv (key ))
203+ }
0 commit comments