Skip to content

Commit 3f779b9

Browse files
authored
fix: H11 provider-registry drift (minimax_* missing from runtime map, docs 12→15) (#39)
* fix(client): register minimax_token_plan and minimax_payg in OpenAICompatibleProviders + DetectProvider * test(client): add provider-registry drift test to prevent future mismatches * docs: sync provider counts (12 → 15) in CREDENTIAL-SETUP-FLOW and DYNAMIC-MODEL-DISCOVERY * test(client): make provider-registry drift test robust to test shuffling The drift test was reading CoreProviders/OpenAICompatibleProviders directly, so any test that called RegisterDynamicProvider at runtime (e.g. TestDynamicProvider_OptIn_Registers) would pollute the maps and cause TestProviderRegistry_NoDriftFromCatalog to fail under -shuffle=on. Capture the static maps once at init() time into staticProviderNames and compare against the snapshot. This is the same robustness pattern used by other tests in the package.
1 parent 8ae1a2a commit 3f779b9

5 files changed

Lines changed: 96 additions & 4 deletions

File tree

client/provider_registry.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ var OpenAICompatibleProviders = map[string]ProviderRegistryConfig{
6464
"kimi": {Name: "kimi", Type: ProviderTypeOpenAICompatible, BaseURL: config.DefaultKimiOpenAIBaseURL, EnvKey: "MOONSHOT_API_KEY", SupportsStreaming: true, SupportsTools: true, SupportsReasoning: true},
6565
"xiaomi_mimo_payg": {Name: "xiaomi_mimo_payg", Type: ProviderTypeOpenAICompatible, BaseURL: config.DefaultXiaomiOpenAIBaseURL, EnvKey: config.EnvXiaomiPaygAPIKey, SupportsStreaming: true, SupportsTools: true, SupportsReasoning: true},
6666
"xiaomi_mimo_token_plan": {Name: "xiaomi_mimo_token_plan", Type: ProviderTypeOpenAICompatible, BaseURL: "", EnvKey: config.EnvXiaomiTokenPlanAPIKey, SupportsStreaming: true, SupportsTools: true, SupportsReasoning: true},
67+
"minimax_token_plan": {Name: "minimax_token_plan", Type: ProviderTypeOpenAICompatible, BaseURL: "https://api.minimax.io/v1", EnvKey: "MINIMAX_TOKEN_PLAN_API_KEY", SupportsStreaming: true, SupportsTools: true, SupportsReasoning: true},
68+
"minimax_payg": {Name: "minimax_payg", Type: ProviderTypeOpenAICompatible, BaseURL: "https://api.minimax.io/v1", EnvKey: "MINIMAX_PAYG_API_KEY", SupportsStreaming: true, SupportsTools: true, SupportsReasoning: true},
6769
}
6870

6971
// GetProviders lists all available providers.
@@ -239,6 +241,12 @@ func DetectProvider() string {
239241
"xiaomi_mimo_token_plan": func() bool {
240242
return credentials.HasSecret(ctx, config.EnvXiaomiTokenPlanAPIKey)
241243
},
244+
"minimax_token_plan": func() bool {
245+
return credentials.HasSecret(ctx, "MINIMAX_TOKEN_PLAN_API_KEY")
246+
},
247+
"minimax_payg": func() bool {
248+
return credentials.HasSecret(ctx, "MINIMAX_PAYG_API_KEY")
249+
},
242250
"ollama": func() bool { return resolveEnvSecret("OLLAMA_BASE_URL") != "" },
243251
"azure": func() bool {
244252
return credentials.HasSecret(ctx, "AZURE_OPENAI_API_KEY") && resolveEnvSecret("AZURE_OPENAI_ENDPOINT") != ""
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//nolint:errcheck
2+
package client
3+
4+
import (
5+
"sort"
6+
"testing"
7+
8+
"github.com/GrayCodeAI/eyrie/catalog/registry"
9+
)
10+
11+
// staticProviderNames is a snapshot of every provider in the static runtime
12+
// maps, captured at init() time. Tests that mutate the maps at runtime (e.g.
13+
// TestDynamicProvider_OptIn_Registers calling RegisterDynamicProvider) would
14+
// otherwise corrupt a naive len()-based drift check, so we always compare
15+
// against this snapshot.
16+
var staticProviderNames map[string]bool
17+
18+
func init() {
19+
staticProviderNames = make(map[string]bool, len(CoreProviders)+len(OpenAICompatibleProviders))
20+
for k := range CoreProviders {
21+
staticProviderNames[k] = true
22+
}
23+
for k := range OpenAICompatibleProviders {
24+
staticProviderNames[k] = true
25+
}
26+
}
27+
28+
// TestProviderRegistry_NoDriftFromCatalog guards against future drift between
29+
// the static runtime provider maps (CoreProviders, OpenAICompatibleProviders)
30+
// and the authoritative spec list in catalog/registry. If a new ProviderSpec
31+
// is added to catalog/registry/providers.go without a corresponding static
32+
// runtime entry, or vice versa, the test fails and the PR is blocked.
33+
//
34+
// This regression test was introduced for H11 (provider-registry drift):
35+
// minimax_token_plan and minimax_payg were present in catalog/registry but
36+
// missing from OpenAICompatibleProviders, causing NewOpenAICompatibleClient
37+
// to mis-route callers to NewOpenAIClient with BaseURL="" (4xx).
38+
func TestProviderRegistry_NoDriftFromCatalog(t *testing.T) {
39+
specs := registry.All()
40+
specNames := make(map[string]bool, len(specs))
41+
for _, s := range specs {
42+
specNames[s.ProviderID] = true
43+
}
44+
45+
// Every spec must be present in the static runtime snapshot.
46+
var missingInRuntime []string
47+
for _, s := range specs {
48+
if !staticProviderNames[s.ProviderID] {
49+
missingInRuntime = append(missingInRuntime, s.ProviderID)
50+
}
51+
}
52+
if len(missingInRuntime) > 0 {
53+
sort.Strings(missingInRuntime)
54+
t.Fatalf(
55+
"provider-registry drift: %d spec(s) in catalog/registry/providers.go "+
56+
"are missing from the static runtime maps: %v",
57+
len(missingInRuntime), missingInRuntime,
58+
)
59+
}
60+
61+
// Every static runtime entry must be present in catalog/registry.
62+
var staleInRuntime []string
63+
for name := range staticProviderNames {
64+
if !specNames[name] {
65+
staleInRuntime = append(staleInRuntime, name)
66+
}
67+
}
68+
if len(staleInRuntime) > 0 {
69+
sort.Strings(staleInRuntime)
70+
t.Fatalf(
71+
"provider-registry drift: %d provider(s) in client/provider_registry.go "+
72+
"(static) are missing from catalog/registry/providers.go: %v",
73+
len(staleInRuntime), staleInRuntime,
74+
)
75+
}
76+
}

docs/ARCHITECTURE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ Auto-detects active provider from env vars in priority order:
104104
| 7 | `ZAI_API_KEY` | <img src="https://cdn.jsdelivr.net/gh/lucide-icons/lucide@latest/icons/bot.svg" width="16" height="16" alt="bot" /> ZAI |
105105
| 8 || <img src="https://cdn.jsdelivr.net/gh/lucide-icons/lucide@latest/icons/server.svg" width="16" height="16" alt="server" /> Ollama (localhost socket) |
106106

107+
*Top 8 by priority; 7 more (`azure`, `bedrock`, `vertex`, `deepseek`, `opencodego`, `kimi`, `xiaomi_mimo_payg`, `xiaomi_mimo_token_plan`, `minimax_token_plan`, `minimax_payg`) — see [`CREDENTIAL-SETUP-FLOW.md`](./guides/CREDENTIAL-SETUP-FLOW.md).*
108+
107109
---
108110

109111
## <img src="https://cdn.jsdelivr.net/gh/lucide-icons/lucide@latest/icons/radio.svg" width="16" height="16" alt="radio" /> Streaming

docs/guides/CREDENTIAL-SETUP-FLOW.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
See also: [DYNAMIC-MODEL-DISCOVERY.md](./DYNAMIC-MODEL-DISCOVERY.md)
66

7-
## Supported providers (12 — from `catalog/registry`)
7+
## Supported providers (15 — from `catalog/registry`)
88

99
| Provider | ID | Credential | Picker models |
1010
|----------|-----|------------|---------------|
1111
| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` | live `/v1/models` only |
1212
| OpenAI | `openai` | `OPENAI_API_KEY` | live `/v1/models` only |
1313
| Google Gemini | `gemini` | `GEMINI_API_KEY` | live models API only |
14+
| DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | live `/models` only |
1415
| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | live `/models` only |
1516
| xAI (Grok) | `grok` | `XAI_API_KEY` | live `/v1/models` only |
1617
| Z.AI | `z-ai` | `ZAI_API_KEY` | live `/models` only |
@@ -19,6 +20,8 @@ See also: [DYNAMIC-MODEL-DISCOVERY.md](./DYNAMIC-MODEL-DISCOVERY.md)
1920
| Kimi (Moonshot) | `kimi` | `MOONSHOT_API_KEY` | live `/models` only |
2021
| Xiaomi (MiMo) Pay-as-you-go | `xiaomi_mimo_payg` | `XIAOMI_MIMO_PAYG_API_KEY` | `https://api.xiaomimimo.com/v1` (`sk-` keys) |
2122
| Xiaomi (MiMo) Token Plan | `xiaomi_mimo_token_plan` | `XIAOMI_MIMO_TOKEN_PLAN_API_KEY` | region: `cn` / `sgp` / `ams` (`tp-` keys) |
23+
| MiniMax (minimax) Token Plan | `minimax_token_plan` | `MINIMAX_TOKEN_PLAN_API_KEY` | `https://api.minimax.io/v1` |
24+
| MiniMax (minimax) Pay-as-you-go | `minimax_payg` | `MINIMAX_PAYG_API_KEY` | `https://api.minimax.io/v1` |
2225
| Ollama (local) | `ollama` | `OLLAMA_BASE_URL` | live `/api/tags` only |
2326

2427
Single source: `eyrie/catalog/registry/providers.go`

docs/guides/DYNAMIC-MODEL-DISCOVERY.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ User → Hawk /config
3737

3838
| Area | Problem |
3939
|------|---------|
40-
| **Live fetch** | All 12 setup gateways have fetchers; some older gateways still have thin test coverage (z-ai, opencodego, kimi). MiMo split: `xiaomi_mimo_payg`, `xiaomi_mimo_token_plan` (`catalog/live/xiaomi_test.go`, `catalog/xiaomi/`). Anthropic, Gemini, Ollama RawJSON gaps remain |
40+
| **Live fetch** | All 15 setup gateways have fetchers; some older gateways still have thin test coverage (z-ai, opencodego, kimi). MiMo split: `xiaomi_mimo_payg`, `xiaomi_mimo_token_plan` (`catalog/live/xiaomi_test.go`, `catalog/xiaomi/`). Anthropic, Gemini, Ollama RawJSON gaps remain |
4141
| **Ollama** | No longer bypasses `ListModels`; RetryConfig moved to ProviderSpec. Remaining: hardcoded `== "ollama"` in validation |
4242
| **Registry drift** | ✅ Fixed — `CredentialProviderRegistry` and `liveDiscoverableDeployments` removed; `DefaultDeploymentEnvFallbacks` consolidated (Item 1) |
4343
| **Layering** | Hawk still has ~112 files with direct eyrie imports (Phase A facade done, B-D remain) |
4444
| **Legacy API** | `FetchModelCatalog` / `providers.go` slices coexist with catalog v1 |
4545
| **Merge policy** | ✅ Live replace — prefer-live providers fully replace models; offerings merge pricing/metadata |
4646
| **Display names** | `BuildSetupUI` has partial hardcoded provider labels |
47-
| **Docs** |`CREDENTIAL-SETUP-FLOW.md` lists all 12 gateways (incl. MiMo payg + token plan) with live-only picker |
47+
| **Docs** |`CREDENTIAL-SETUP-FLOW.md` lists all 15 gateways (incl. MiMo payg + token plan, DeepSeek, MiniMax token plan + payg) with live-only picker |
4848

4949
---
5050

@@ -280,6 +280,7 @@ Provider-specific friendly text lives in eyrie, not hawk cmd.
280280
| **Anthropic** | `ANTHROPIC_API_KEY` | `GET /v1/models` | `/v1/models` fetcher | Rate limits on list |
281281
| **OpenAI** | `OPENAI_API_KEY` | `GET /v1/models` | `/v1/models` | Org-scoped model lists differ |
282282
| **Gemini** | `GEMINI_API_KEY` | `GET /v1beta/models` | Gemini models API | Key in query param |
283+
| **DeepSeek** | `DEEPSEEK_API_KEY` | `GET /models` | OpenAI-compat `/models` | `https://api.deepseek.com/v1` |
283284
| **OpenRouter** | `OPENROUTER_API_KEY` | `GET /models` | Already live | Largest dynamic catalog |
284285
| **Grok/xAI** | `XAI_API_KEY` | `GET /v1/models` | `/v1/models` | OpenAI-compatible |
285286
| **Z.AI** | `ZAI_API_KEY` | `GET /models` | OpenAI-compat `/models` | Base URL env fallbacks |
@@ -288,6 +289,8 @@ Provider-specific friendly text lives in eyrie, not hawk cmd.
288289
| **Kimi (Moonshot)** | `MOONSHOT_API_KEY` | `GET /models` | OpenAI-compat `/models` | Provider id `kimi` |
289290
| **Xiaomi (MiMo) Pay-as-you-go** | `XIAOMI_MIMO_PAYG_API_KEY` | `GET /v1/models` (`api-key`; Bearer on 401) | OpenAI: `api.xiaomimimo.com/v1` · Anthropic: `api.xiaomimimo.com/anthropic` | `xiaomi_mimo_payg`; chat via `MiMoClient` |
290291
| **Xiaomi (MiMo) Token Plan** | `XIAOMI_MIMO_TOKEN_PLAN_API_KEY` | `GET /v1/models` (region host) | OpenAI + Anthropic per region (`token-plan-{cn,sgp,ams}.xiaomimimo.com`) | `xiaomi_mimo_token_plan` + `xiaomi_mimo_token_plan_region` in provider.json |
292+
| **MiniMax (minimax) Token Plan** | `MINIMAX_TOKEN_PLAN_API_KEY` | `GET /v1/models` | OpenAI-compat `/v1/models` | `https://api.minimax.io/v1` |
293+
| **MiniMax (minimax) Pay-as-you-go** | `MINIMAX_PAYG_API_KEY` | `GET /v1/models` | OpenAI-compat `/v1/models` | `https://api.minimax.io/v1` |
291294
| **Ollama** | `OLLAMA_BASE_URL` | `GET /api/tags` | `/api/tags` | Zero models = error; no remote fallback in picker |
292295

293296
### Model discovery (all setup providers)
@@ -326,7 +329,7 @@ DiscoverCatalog(ctx, opts)
326329
327330
## 8. Merge policy
328331
329-
**Implemented:** `discover.MergeCatalogV1WithPolicy` replaces deployment offerings from live fetch, then **fully replaces** model rows for prefer-live providers (all 12 setup gateways). Offerings merge pricing, capabilities, and `live_metadata` from the live catalog.
332+
**Implemented:** `discover.MergeCatalogV1WithPolicy` replaces deployment offerings from live fetch, then **fully replaces** model rows for prefer-live providers (all 15 setup gateways). Offerings merge pricing, capabilities, and `live_metadata` from the live catalog.
330333
331334
Remote catalog JSON still supplies deployments, protocols, and bootstrap metadata — not picker model IDs for setup gateways.
332335

0 commit comments

Comments
 (0)