From ffe86284d20f90bd1577820e82339d1d1c88e929 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 19 Apr 2026 10:20:32 +0200 Subject: [PATCH] model-registry: split direct vs family_failover providers per model family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/core/model-registry.ts | 62 ++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/packages/pi-coding-agent/src/core/model-registry.ts b/packages/pi-coding-agent/src/core/model-registry.ts index b59e82097..b1d9e3b42 100644 --- a/packages/pi-coding-agent/src/core/model-registry.ts +++ b/packages/pi-coding-agent/src/core/model-registry.ts @@ -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[] { 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]; } /**