model-registry: split direct vs family_failover providers per model family

Prior PROXY_FAMILY_PRIORITY table conflated "direct provider" with
"failover provider that happens to serve this family". Observed case:
claude-* family listed anthropic, google-antigravity, and
github-copilot all as "providers" — but only anthropic is the direct
vendor. google-antigravity re-serves Claude via Google's sandbox
IDE product (same endpoint as gemini-cli, different auth contract);
github-copilot re-serves via GitHub's paid platform.

This matters for the 429 fallback chain: a broken anthropic key
should try genuinely-vendored endpoints first (none, for Claude),
then fall into family_failover (antigravity, copilot), and only then
reach the generic GLOBAL_PROVIDER_FALLBACK (opencode, opencode-go,
openrouter, ollama-cloud). The old all-flat list hid this distinction.

New shape:
  { providers: [...], family_failover?: [...] }

Corrections applied:
  claude-*: providers=[anthropic], failover=[google-antigravity, github-copilot]
  gemini-*: providers=[google-gemini-cli, google, google-vertex],
            failover=[github-copilot]
  gpt-* / o* / codex-*: providers=[openai],
            failover=[azure-openai-responses, openai-codex, github-copilot]
  mimo-*: providers=[xiaomi]  (new: was [] — Xiaomi MiMo Open Platform
          is direct API at api.xiaomimimo.com / token-plan-sgp.xiaomimimo.com)

buildCandidateOrder stitches [direct, family_failover, global_fallback]
with deduplication. User overrides via settings.proxy.providerPriority
continue to replace only the direct-provider list, keeping family
failover and global fallback intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-04-19 10:20:32 +02:00
parent 0f0dcbf8c7
commit ffe86284d2

View file

@ -55,22 +55,51 @@ export const PROXY_FAMILY_PRIORITY: ReadonlyArray<{
match: RegExp;
/** Canonical key used when matching settings.proxy.providerPriority overrides */
prefix: string;
/** True direct providers — the vendor or first-party endpoint. Tried first. */
providers: string[];
/**
* Family-scoped failover providers re-servers/proxies that serve this
* family but aren't the vendor. Tried AFTER direct providers but BEFORE
* the generic GLOBAL_PROVIDER_FALLBACK. Kept separate so the config is
* honest about which endpoints are "native" vs "via intermediary".
*/
family_failover?: string[];
}> = [
// minimax direct (api.minimax.io) → CN endpoint
// MiniMax direct (api.minimax.io) → CN endpoint as its direct pair
{ match: /^MiniMax-/i, prefix: "MiniMax-", providers: ["minimax", "minimax-cn"] },
// ZAI direct API for GLM
{ match: /^glm-/i, prefix: "glm-", providers: ["zai"] },
// Kimi Code direct API
{ match: /^kimi-/i, prefix: "kimi-", providers: ["kimi-coding"] },
// MiMo/Xiaomi — no direct provider; resolved entirely via global fallback
{ match: /^mimo-|^XiaomiMiMo\//i, prefix: "mimo-", providers: [] },
// Gemini/Gemma: free CLI (OAuth) first, then paid API tiers
{ match: /^gemini-|^gemma-/i, prefix: "gemini-", providers: ["google-gemini-cli", "google", "google-vertex", "github-copilot"] },
// Claude: Anthropic direct, then OAuth/delegated
{ match: /^claude-/i, prefix: "claude-", providers: ["anthropic", "google-antigravity", "github-copilot"] },
// GPT / o-series / codex
{ match: /^gpt-|^o\d|^codex-/i, prefix: "gpt-", providers: ["openai", "azure-openai-responses", "github-copilot"] },
// MiMo/Xiaomi — direct API via Xiaomi MiMo Open Platform (api.xiaomimimo.com)
// or the Token Plan endpoint (token-plan-sgp.xiaomimimo.com). Both served
// under the `xiaomi` provider namespace.
{ match: /^mimo-|^XiaomiMiMo\//i, prefix: "mimo-", providers: ["xiaomi"] },
// Gemini/Gemma: google-gemini-cli (OAuth), google (API key), google-vertex
// are all FIRST-PARTY Google endpoints. github-copilot re-serves and is
// failover only.
{
match: /^gemini-|^gemma-/i, prefix: "gemini-",
providers: ["google-gemini-cli", "google", "google-vertex"],
family_failover: ["github-copilot"],
},
// Claude: Anthropic is the ONLY direct provider.
// google-antigravity and github-copilot re-serve Claude via Google's and
// GitHub's own platforms — failover, not direct.
{
match: /^claude-/i, prefix: "claude-",
providers: ["anthropic"],
family_failover: ["google-antigravity", "github-copilot"],
},
// GPT / o-series / codex: OpenAI is direct. azure-openai-responses is
// Microsoft's re-serving of OpenAI weights — treated as failover (it is
// the same weights via a different legal/contractual relationship).
// github-copilot likewise re-serves.
{
match: /^gpt-|^o\d|^codex-/i, prefix: "gpt-",
providers: ["openai"],
family_failover: ["azure-openai-responses", "openai-codex", "github-copilot"],
},
];
// ── Schema for OpenRouter routing preferences
@ -822,13 +851,16 @@ export class ModelRegistry {
private buildCandidateOrder(modelId: string, overrides: Record<string, string[]>): string[] {
const overrideEntry = Object.entries(overrides).find(([k]) => modelId.startsWith(k));
const familyProviders =
overrideEntry?.[1] ??
PROXY_FAMILY_PRIORITY.find((r) => r.match.test(modelId))?.providers ??
[];
const seen = new Set(familyProviders);
const familyEntry = PROXY_FAMILY_PRIORITY.find((r) => r.match.test(modelId));
// Order: direct family providers → family-scoped failover → global fallback.
// Overrides replace only the direct list (keeps family_failover + global
// chain intact) so a user pinning "glm- → [zai]" still picks up
// opencode-go / openrouter / ollama-cloud as last resort.
const familyProviders = overrideEntry?.[1] ?? familyEntry?.providers ?? [];
const familyFailover = familyEntry?.family_failover ?? [];
const seen = new Set([...familyProviders, ...familyFailover]);
const globalFallback = GLOBAL_PROVIDER_FALLBACK.filter((p) => !seen.has(p));
return [...familyProviders, ...globalFallback];
return [...familyProviders, ...familyFailover, ...globalFallback];
}
/**