Skip to content

Commit f3abc3b

Browse files
authored
feat(catalog,client,setup): Z.AI proper plan separation (z-ai general + z-ai-coding) with dual OpenAI+Anthropic endpoints and region support (#36)
* feat(catalog,client,setup): Z.AI proper plan separation (z-ai general + z-ai-coding) with dual OpenAI+Anthropic endpoints and region support * fix(setup): nil-check cfg in Z.AI deployment resolvers resolveZAIOpenAIBaseForDeployment and resolveZAIAnthropicBaseForDeployment panicked when config.LoadProviderConfig("") returned nil (no provider.json configured). Add the nil guard so deployment lookup is safe for first-run users without a provider config.
1 parent c0e2779 commit f3abc3b

32 files changed

Lines changed: 805 additions & 150 deletions

catalog/deployment_env_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ func TestDefaultDeploymentEnvFallbacks_GrokHasXAIAPIKey(t *testing.T) {
4848

4949
func TestDefaultDeploymentEnvFallbacks_ZAIHasZAIAPIBase(t *testing.T) {
5050
fbs := DefaultDeploymentEnvFallbacks
51-
zai, ok := fbs["z-ai-direct"]
51+
zai, ok := fbs["zai_payg-direct"]
5252
if !ok {
53-
t.Fatal("z-ai-direct not found in env fallbacks")
53+
t.Fatal("zai_payg-direct not found in env fallbacks")
5454
}
5555
hasZAIAPIBase := false
5656
for _, fb := range zai {
@@ -63,7 +63,7 @@ func TestDefaultDeploymentEnvFallbacks_ZAIHasZAIAPIBase(t *testing.T) {
6363
}
6464
}
6565
if !hasZAIAPIBase {
66-
t.Error("z-ai-direct base_url fallbacks should include ZAI_API_BASE")
66+
t.Error("zai_payg-direct base_url fallbacks should include ZAI_API_BASE")
6767
}
6868
}
6969

catalog/live/fetchers.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const (
2424
DefaultOpenRouterBaseURL = "https://openrouter.ai/api/v1"
2525
DefaultCanopyWaveBaseURL = "https://inference.canopywave.io/v1"
2626
DefaultZAIBaseURL = "https://api.z.ai/api/paas/v4"
27+
DefaultZAICodingBaseURL = "https://api.z.ai/api/coding/paas/v4"
2728
DefaultOpenAIBaseURL = "https://api.openai.com/v1"
2829
DefaultGrokBaseURL = "https://api.x.ai/v1"
2930
DefaultOpenCodeGoBaseURL = opencodego.DefaultBaseURL
@@ -47,7 +48,8 @@ var Registry = map[string]FetchFunc{
4748
"vertex": FetchVertex,
4849
"openrouter": FetchOpenRouter,
4950
"grok": FetchGrok,
50-
"z-ai": FetchZAI,
51+
"zai_payg": FetchZAI,
52+
"zai_coding": FetchZAICoding,
5153
"canopywave": FetchCanopyWave,
5254
"opencodego": FetchOpenCodeGo,
5355
"kimi": FetchKimi,
@@ -739,6 +741,17 @@ func FetchZAI(env map[string]string) ([]Entry, error) {
739741
return entries, nil
740742
}
741743

744+
// FetchZAICoding lists models using the GLM Coding Plan dedicated endpoint.
745+
// It expects ZAI_CODING_API_KEY (and optional ZAI_CODING_BASE_URL) in the env map.
746+
// This ensures proper quota/billing separation from the general pay-as-you-go path.
747+
func FetchZAICoding(env map[string]string) ([]Entry, error) {
748+
return fetchOpenAICompatModels(
749+
context.Background(),
750+
envOr(env, "ZAI_CODING_BASE_URL", DefaultZAICodingBaseURL),
751+
env["ZAI_CODING_API_KEY"], "Bearer",
752+
)
753+
}
754+
742755
func FetchCanopyWave(env map[string]string) ([]Entry, error) {
743756
entries, err := fetchOpenAICompatModels(
744757
context.Background(),

catalog/live/zai_test.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ func TestFetchZAI_MockHTTPServer(t *testing.T) {
2121
Data []json.RawMessage `json:"data"`
2222
}{
2323
Data: []json.RawMessage{
24-
json.RawMessage(`{"id":"z-ai/model-a","display_name":"ZAI Model A","status":1}`),
25-
json.RawMessage(`{"id":"z-ai/model-b","display_name":"ZAI Model B","status":1}`),
24+
json.RawMessage(`{"id":"zai_payg/model-a","display_name":"ZAI Model A","status":1}`),
25+
json.RawMessage(`{"id":"zai_payg/model-b","display_name":"ZAI Model B","status":1}`),
2626
},
2727
}
2828
_ = json.NewEncoder(w).Encode(resp)
@@ -43,9 +43,9 @@ func TestFetchZAI_MockHTTPServer(t *testing.T) {
4343
for _, e := range entries {
4444
byID[e.ID] = e
4545
}
46-
a, ok := byID["z-ai/model-a"]
46+
a, ok := byID["zai_payg/model-a"]
4747
if !ok {
48-
t.Fatal("missing z-ai/model-a")
48+
t.Fatal("missing zai_payg/model-a")
4949
}
5050
if a.DisplayName != "ZAI Model A" {
5151
t.Fatalf("display name = %q", a.DisplayName)
@@ -80,3 +80,51 @@ func TestFetchZAI_Unauthorized(t *testing.T) {
8080
t.Fatal("expected error for 401")
8181
}
8282
}
83+
84+
func TestFetchZAICoding_MockHTTPServer(t *testing.T) {
85+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
86+
if r.URL.Path != "/models" {
87+
http.NotFound(w, r)
88+
return
89+
}
90+
if r.Header.Get("Authorization") != "Bearer test-coding-key" {
91+
w.WriteHeader(http.StatusUnauthorized)
92+
return
93+
}
94+
resp := struct {
95+
Data []json.RawMessage `json:"data"`
96+
}{
97+
Data: []json.RawMessage{
98+
json.RawMessage(`{"id":"zai_coding/model-c","display_name":"ZAI Coding Model C","status":1}`),
99+
json.RawMessage(`{"id":"zai_coding/model-d","display_name":"ZAI Coding Model D","status":1}`),
100+
},
101+
}
102+
_ = json.NewEncoder(w).Encode(resp)
103+
}))
104+
defer srv.Close()
105+
106+
entries, err := FetchZAICoding(map[string]string{
107+
"ZAI_CODING_API_KEY": "test-coding-key",
108+
"ZAI_CODING_BASE_URL": srv.URL,
109+
})
110+
if err != nil {
111+
t.Fatal(err)
112+
}
113+
if len(entries) != 2 {
114+
t.Fatalf("expected 2 models, got %d", len(entries))
115+
}
116+
byID := map[string]Entry{}
117+
for _, e := range entries {
118+
byID[e.ID] = e
119+
}
120+
c, ok := byID["zai_coding/model-c"]
121+
if !ok {
122+
t.Fatal("missing zai_coding/model-c")
123+
}
124+
if c.DisplayName != "ZAI Coding Model C" {
125+
t.Fatalf("display name = %q", c.DisplayName)
126+
}
127+
if len(c.RawJSON) == 0 {
128+
t.Fatal("expected RawJSON to be preserved")
129+
}
130+
}

catalog/live_catalog_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
func TestIsLiveOnlyProvider(t *testing.T) {
1010
// All providers are now fully dynamic
1111
allProviders := []string{
12-
"anthropic", "openai", "gemini", "grok", "canopywave", "z-ai", "openrouter", "ollama", "opencodego",
12+
"anthropic", "openai", "gemini", "grok", "canopywave", "z_ai", "openrouter", "ollama", "opencodego",
1313
"azure", "bedrock", "vertex", "kimi", "xiaomi_mimo_payg", "xiaomi_mimo_token_plan", "deepseek",
1414
}
1515
for _, p := range allProviders {
@@ -21,12 +21,12 @@ func TestIsLiveOnlyProvider(t *testing.T) {
2121

2222
func TestFirstModelForProvider(t *testing.T) {
2323
c := catalog.TestSeedCatalogV1()
24-
c.Models["z-ai/glm-5.1"] = catalog.ModelV1{ID: "z-ai/glm-5.1", ProviderID: "z-ai", Name: "GLM-5.1"}
24+
c.Models["zai_payg/glm-5.1"] = catalog.ModelV1{ID: "zai_payg/glm-5.1", ProviderID: "zai_payg", Name: "GLM-5.1"}
2525
compiled, err := catalog.CompileCatalogV1(&c)
2626
if err != nil {
2727
t.Fatal(err)
2828
}
29-
if got := catalog.FirstModelForProvider(compiled, "z-ai"); got != "z-ai/glm-5.1" {
29+
if got := catalog.FirstModelForProvider(compiled, "zai_payg"); got != "zai_payg/glm-5.1" {
3030
t.Fatalf("FirstModelForProvider = %q", got)
3131
}
3232
}

catalog/model_tiers_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func TestAllProvidersAreLiveOnly(t *testing.T) {
7575
// All providers should now return nil candidates (fully dynamic)
7676
allProviders := []string{
7777
"anthropic", "openai", "gemini", "grok", "opencodego",
78-
"canopywave", "z-ai", "openrouter", "ollama",
78+
"canopywave", "z_ai", "openrouter", "ollama",
7979
"azure", "bedrock", "vertex", "kimi",
8080
"xiaomi_mimo_payg", "xiaomi_mimo_token_plan", "deepseek",
8181
}

catalog/provider_credentials_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ func TestPrimaryAPIKeyEnvForProvider(t *testing.T) {
1717
if got := PrimaryAPIKeyEnvForProvider(compiled, "canopywave"); got != "CANOPYWAVE_API_KEY" {
1818
t.Fatalf("canopywave env = %q", got)
1919
}
20-
if got := PrimaryAPIKeyEnvForProvider(compiled, "z-ai"); got != "ZAI_API_KEY" {
21-
t.Fatalf("z-ai env = %q", got)
20+
if got := PrimaryAPIKeyEnvForProvider(compiled, "zai_payg"); got != "ZAI_API_KEY" {
21+
t.Fatalf("zai_payg env = %q", got)
22+
}
23+
if got := PrimaryAPIKeyEnvForProvider(compiled, "zai_coding"); got != "ZAI_CODING_API_KEY" {
24+
t.Fatalf("zai_coding env = %q", got)
2225
}
2326
}
2427

catalog/provider_live_parity_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010

1111
func TestAllProviders_LiveFetchParity(t *testing.T) {
1212
specs := registry.All()
13-
if len(specs) != 18 {
14-
t.Fatalf("expected 16 providers, got %d", len(specs))
13+
if len(specs) != 19 {
14+
t.Fatalf("expected 19 providers, got %d", len(specs))
1515
}
1616
for _, spec := range specs {
1717
t.Run(spec.ProviderID, func(t *testing.T) {

catalog/provider_registration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
)
88

99
func TestSpecByProviderID_RegisteredProviders(t *testing.T) {
10-
providers := []string{"anthropic", "openai", "gemini", "grok", "openrouter", "z-ai", "canopywave", "ollama", "opencodego", "kimi", "xiaomi_mimo_payg", "xiaomi_mimo_token_plan"}
10+
providers := []string{"anthropic", "openai", "gemini", "grok", "openrouter", "zai_payg", "zai_coding", "canopywave", "ollama", "opencodego", "kimi", "xiaomi_mimo_payg", "xiaomi_mimo_token_plan"}
1111
for _, id := range providers {
1212
spec, ok := SpecByProviderID(id)
1313
if !ok {

catalog/registry/provider_spec_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88
)
99

1010
func TestAllProviders_Count(t *testing.T) {
11-
if n := len(registry.All()); n != 18 {
12-
t.Fatalf("expected 18 providers, got %d", n)
11+
if n := len(registry.All()); n != 19 {
12+
t.Fatalf("expected 19 providers, got %d", n)
1313
}
1414
}
1515

@@ -21,8 +21,8 @@ func TestCredentialRegistry_MatchesAll(t *testing.T) {
2121

2222
func TestLiveFetcherKeys_AllProviders(t *testing.T) {
2323
keys := registry.LiveFetcherKeys()
24-
if len(keys) != 18 {
25-
t.Fatalf("expected 18 live fetcher keys, got %d", len(keys))
24+
if len(keys) != 19 {
25+
t.Fatalf("expected 19 live fetcher keys, got %d", len(keys))
2626
}
2727
}
2828

@@ -59,7 +59,8 @@ func TestProviderSpecs_TableDriven(t *testing.T) {
5959
{"vertex", "vertex", true, registry.ProbeNone, true, "gemini-vertex"},
6060
{"openrouter", "openrouter", true, registry.ProbeOpenAIModels, true, "openrouter"},
6161
{"grok", "grok", true, registry.ProbeOpenAIModels, true, "grok-direct"},
62-
{"z-ai", "z-ai", true, registry.ProbeOpenAIModels, true, "z-ai-direct"},
62+
{"zai_payg", "zai_payg", true, registry.ProbeOpenAIModels, true, "zai_payg-direct"},
63+
{"zai_coding", "zai_coding", true, registry.ProbeOpenAIModels, true, "zai_coding-direct"},
6364
{"canopywave", "canopywave", true, registry.ProbeOpenAIModels, true, "canopywave"},
6465
{"opencodego", "opencodego", true, registry.ProbeOpenAIModels, true, "opencodego"},
6566
{"kimi", "kimi", true, registry.ProbeOpenAIModels, true, "kimi-direct"},

catalog/registry/providers.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,39 +65,47 @@ func providerSpecs() []ProviderSpec {
6565
APIProtocolID: "openai-chat-completions", AdapterID: "kimi",
6666
},
6767
{
68-
ProviderID: "z-ai", DisplayName: "Z.AI", DeploymentID: "z-ai-direct", SortOrder: 7,
68+
ProviderID: "zai_coding", DisplayName: "Z.AI — Coding Plan", DeploymentID: "zai_coding-direct", SortOrder: 7,
69+
RequiresKey: true, CredentialEnv: "ZAI_CODING_API_KEY",
70+
BaseURLEnv: []string{"ZAI_CODING_BASE_URL", "ZAI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
71+
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.z.ai/api/coding/paas/v4",
72+
LiveFetcherKey: "zai_coding", LiveCatalogKey: "zai_coding",
73+
APIProtocolID: "openai-chat-completions", AdapterID: "zai_coding",
74+
},
75+
{
76+
ProviderID: "zai_payg", DisplayName: "Z.AI — Pay-as-you-go", DeploymentID: "zai_payg-direct", SortOrder: 8,
6977
RequiresKey: true, CredentialEnv: "ZAI_API_KEY",
7078
BaseURLEnv: []string{"ZAI_BASE_URL", "ZAI_API_BASE", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
7179
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.z.ai/api/paas/v4",
72-
LiveFetcherKey: "z-ai", LiveCatalogKey: "z-ai",
73-
APIProtocolID: "openai-chat-completions", AdapterID: "z-ai",
80+
LiveFetcherKey: "zai_payg", LiveCatalogKey: "zai_payg",
81+
APIProtocolID: "openai-chat-completions", AdapterID: "zai_payg",
7482
},
7583
{
76-
ProviderID: "xiaomi_mimo_token_plan", DisplayName: "Xiaomi MiMo — Token Plan", DeploymentID: "xiaomi_mimo_token_plan-direct", SortOrder: 8,
84+
ProviderID: "xiaomi_mimo_token_plan", DisplayName: "Xiaomi MiMo — Token Plan", DeploymentID: "xiaomi_mimo_token_plan-direct", SortOrder: 9,
7785
RequiresKey: true, CredentialEnv: "XIAOMI_MIMO_TOKEN_PLAN_API_KEY",
7886
BaseURLEnv: []string{"XIAOMI_MIMO_TOKEN_PLAN_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
7987
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "",
8088
LiveFetcherKey: "xiaomi_mimo_token_plan", LiveCatalogKey: "xiaomi_mimo_token_plan",
8189
APIProtocolID: "openai-chat-completions", AdapterID: "xiaomi_mimo",
8290
},
8391
{
84-
ProviderID: "xiaomi_mimo_payg", DisplayName: "Xiaomi MiMo — Pay-as-you-go", DeploymentID: "xiaomi_mimo_payg-direct", SortOrder: 9,
92+
ProviderID: "xiaomi_mimo_payg", DisplayName: "Xiaomi MiMo — Pay-as-you-go", DeploymentID: "xiaomi_mimo_payg-direct", SortOrder: 10,
8593
RequiresKey: true, CredentialEnv: "XIAOMI_MIMO_PAYG_API_KEY",
8694
BaseURLEnv: []string{"XIAOMI_MIMO_PAYG_BASE_URL", "XIAOMI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
8795
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.xiaomimimo.com/v1",
8896
LiveFetcherKey: "xiaomi_mimo_payg", LiveCatalogKey: "xiaomi_mimo_payg",
8997
APIProtocolID: "openai-chat-completions", AdapterID: "xiaomi_mimo",
9098
},
9199
{
92-
ProviderID: "minimax_token_plan", DisplayName: "MiniMax — Token Plan", DeploymentID: "minimax_token_plan-direct", SortOrder: 10,
100+
ProviderID: "minimax_token_plan", DisplayName: "MiniMax — Token Plan", DeploymentID: "minimax_token_plan-direct", SortOrder: 11,
93101
RequiresKey: true, CredentialEnv: "MINIMAX_TOKEN_PLAN_API_KEY",
94102
BaseURLEnv: []string{"MINIMAX_TOKEN_PLAN_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
95103
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.minimax.io/v1",
96104
LiveFetcherKey: "minimax_token_plan", LiveCatalogKey: "minimax_token_plan",
97105
APIProtocolID: "openai-chat-completions", AdapterID: "openai",
98106
},
99107
{
100-
ProviderID: "minimax_payg", DisplayName: "MiniMax — Pay-as-you-go", DeploymentID: "minimax_payg-direct", SortOrder: 11,
108+
ProviderID: "minimax_payg", DisplayName: "MiniMax — Pay-as-you-go", DeploymentID: "minimax_payg-direct", SortOrder: 12,
101109
RequiresKey: true, CredentialEnv: "MINIMAX_PAYG_API_KEY",
102110
BaseURLEnv: []string{"MINIMAX_PAYG_BASE_URL", "MINIMAX_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
103111
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://api.minimax.io/v1",
@@ -107,15 +115,15 @@ func providerSpecs() []ProviderSpec {
107115

108116
// ── Cloud platform providers ──────────────────────────────────────
109117
{
110-
ProviderID: "azure", DisplayName: "Azure OpenAI", DeploymentID: "openai-azure", SortOrder: 12,
118+
ProviderID: "azure", DisplayName: "Azure OpenAI", DeploymentID: "openai-azure", SortOrder: 13,
111119
RequiresKey: true, CredentialEnv: "AZURE_OPENAI_API_KEY",
112120
BaseURLEnv: []string{"AZURE_OPENAI_ENDPOINT"},
113121
ProbeKind: ProbeNone,
114122
LiveFetcherKey: "azure", LiveCatalogKey: "azure",
115123
APIProtocolID: "openai-chat-completions", AdapterID: "openai-azure",
116124
},
117125
{
118-
ProviderID: "bedrock", DisplayName: "Amazon Bedrock", DeploymentID: "anthropic-bedrock", SortOrder: 13,
126+
ProviderID: "bedrock", DisplayName: "Amazon Bedrock", DeploymentID: "anthropic-bedrock", SortOrder: 14,
119127
RequiresKey: true, CredentialEnv: "AWS_SECRET_ACCESS_KEY",
120128
CredentialEnvFallbacks: []string{"AWS_ACCESS_KEY_ID", "AWS_SESSION_TOKEN"},
121129
BaseURLEnv: []string{"AWS_REGION", "AWS_DEFAULT_REGION"},
@@ -124,7 +132,7 @@ func providerSpecs() []ProviderSpec {
124132
APIProtocolID: "anthropic-messages", AdapterID: "anthropic-bedrock",
125133
},
126134
{
127-
ProviderID: "vertex", DisplayName: "Vertex AI", DeploymentID: "gemini-vertex", SortOrder: 14,
135+
ProviderID: "vertex", DisplayName: "Vertex AI", DeploymentID: "gemini-vertex", SortOrder: 15,
128136
RequiresKey: true, CredentialEnv: "VERTEX_ACCESS_TOKEN",
129137
CredentialEnvFallbacks: []string{"GOOGLE_OAUTH_ACCESS_TOKEN"},
130138
BaseURLEnv: []string{"VERTEX_PROJECT_ID", "VERTEX_REGION"},
@@ -135,7 +143,7 @@ func providerSpecs() []ProviderSpec {
135143

136144
// ── Aggregators ───────────────────────────────────────────────────
137145
{
138-
ProviderID: "openrouter", DisplayName: "OpenRouter", DeploymentID: "openrouter", SortOrder: 15,
146+
ProviderID: "openrouter", DisplayName: "OpenRouter", DeploymentID: "openrouter", SortOrder: 16,
139147
RequiresKey: true, CredentialEnv: "OPENROUTER_API_KEY",
140148
BaseURLEnv: []string{"OPENROUTER_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
141149
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://openrouter.ai/api/v1",
@@ -145,15 +153,15 @@ func providerSpecs() []ProviderSpec {
145153

146154
// ── Niche ─────────────────────────────────────────────────────────
147155
{
148-
ProviderID: "canopywave", DisplayName: "CanopyWave", DeploymentID: "canopywave", SortOrder: 16,
156+
ProviderID: "canopywave", DisplayName: "CanopyWave", DeploymentID: "canopywave", SortOrder: 17,
149157
RequiresKey: true, CredentialEnv: "CANOPYWAVE_API_KEY",
150158
BaseURLEnv: []string{"CANOPYWAVE_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
151159
ProbeKind: ProbeOpenAIModels, ProbeBaseURL: "https://inference.canopywave.io/v1",
152160
LiveFetcherKey: "canopywave", LiveCatalogKey: "canopywave",
153161
APIProtocolID: "openai-chat-completions", AdapterID: "canopywave",
154162
},
155163
{
156-
ProviderID: "opencodego", DisplayName: "OpenCode Go", DeploymentID: "opencodego", SortOrder: 17,
164+
ProviderID: "opencodego", DisplayName: "OpenCode Go", DeploymentID: "opencodego", SortOrder: 18,
157165
RequiresKey: true, CredentialEnv: "OPENCODEGO_API_KEY",
158166
BaseURLEnv: []string{"OPENCODEGO_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE"},
159167
ProbeKind: ProbeOpenAIModels,
@@ -164,7 +172,7 @@ func providerSpecs() []ProviderSpec {
164172

165173
// ── Local ─────────────────────────────────────────────────────────
166174
{
167-
ProviderID: "ollama", DisplayName: "Ollama", DeploymentID: "ollama-local", SortOrder: 18,
175+
ProviderID: "ollama", DisplayName: "Ollama", DeploymentID: "ollama-local", SortOrder: 19,
168176
RequiresKey: false, CredentialEnv: "OLLAMA_BASE_URL",
169177
BaseURLEnv: []string{"OLLAMA_BASE_URL"},
170178
ProbeKind: ProbeOllama,

0 commit comments

Comments
 (0)