diff --git a/workspace-server/internal/providers/gen/registry_gen.go b/workspace-server/internal/providers/gen/registry_gen.go index 286acc4f9..39b5c31f9 100644 --- a/workspace-server/internal/providers/gen/registry_gen.go +++ b/workspace-server/internal/providers/gen/registry_gen.go @@ -16,7 +16,7 @@ const SchemaVersion = 1 // Fingerprint is a stable content hash of the generated projection (schema // version + provider catalog + runtime native sets). It changes iff the // registry DATA changes (comment-only YAML edits do not churn it). -const Fingerprint = "5a741b326b6f812c" +const Fingerprint = "ec6b93409e7b9cf8" // GenProvider is the generated projection of one provider catalog entry — // the subset a downstream consumer needs to derive + display a provider. @@ -71,6 +71,11 @@ var Providers = []GenProvider{ {Name: "nvidia", DisplayName: "NVIDIA NIM", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"NVIDIA_API_KEY"}, ModelPrefixMatch: "^nvidia[:/]", IsPlatform: false}, {Name: "arcee", DisplayName: "Arcee", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"ARCEE_API_KEY"}, ModelPrefixMatch: "^arcee[:/]", IsPlatform: false}, {Name: "custom", DisplayName: "Custom OpenAI-compat endpoint", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"CUSTOM_API_KEY", "OPENAI_API_KEY"}, ModelPrefixMatch: "^custom[:/]", IsPlatform: false}, + {Name: "byok-anthropic", DisplayName: "Anthropic (BYOK)", Protocol: "anthropic", AuthMode: "anthropic_api", AuthEnv: []string{"ANTHROPIC_API_KEY"}, ModelPrefixMatch: "^anthropic/", IsPlatform: false}, + {Name: "byok-openai", DisplayName: "OpenAI (BYOK)", Protocol: "openai", AuthMode: "anthropic_api", AuthEnv: []string{"OPENAI_API_KEY"}, ModelPrefixMatch: "^openai[:/]", IsPlatform: false}, + {Name: "byok-gemini", DisplayName: "Google Gemini (BYOK)", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"GEMINI_API_KEY", "GOOGLE_API_KEY"}, ModelPrefixMatch: "^gemini/", IsPlatform: false}, + {Name: "byok-minimax", DisplayName: "MiniMax (BYOK)", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"MINIMAX_API_KEY"}, ModelPrefixMatch: "(?i)^(minimax[:/]|codex-minimax-)", IsPlatform: false}, + {Name: "groq", DisplayName: "Groq", Protocol: "openai", AuthMode: "third_party_anthropic_compat", AuthEnv: []string{"GROQ_API_KEY"}, ModelPrefixMatch: "^groq:", IsPlatform: false}, } // Runtimes maps each runtime to its native provider+model set, runtime names @@ -90,6 +95,7 @@ var Runtimes = map[string][]GenRuntimeRef{ {Name: "openai-subscription", Models: []string{"gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.2"}}, {Name: "openai-api", Models: []string{"gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.2"}}, {Name: "platform", Models: []string{"openai/gpt-5.4", "openai/gpt-5.4-mini"}}, + {Name: "byok-minimax", Models: []string{}}, }, "google-adk": { {Name: "platform", Models: []string{"platform:gemini-2.5-pro", "platform:gemini-2.5-flash"}}, @@ -114,11 +120,18 @@ var Runtimes = map[string][]GenRuntimeRef{ {Name: "zai", Models: []string{}}, {Name: "xiaomi-mimo", Models: []string{}}, {Name: "alibaba", Models: []string{}}, + {Name: "byok-anthropic", Models: []string{}}, + {Name: "byok-gemini", Models: []string{}}, + {Name: "byok-openai", Models: []string{}}, + {Name: "byok-minimax", Models: []string{}}, }, "openclaw": { {Name: "kimi-coding", Models: []string{"moonshot:kimi-k2.6", "moonshot:kimi-k2.5"}}, {Name: "platform", Models: []string{"moonshot/kimi-k2.6", "moonshot/kimi-k2.5"}}, {Name: "openrouter", Models: []string{}}, {Name: "custom", Models: []string{}}, + {Name: "byok-openai", Models: []string{}}, + {Name: "byok-minimax", Models: []string{}}, + {Name: "groq", Models: []string{}}, }, } diff --git a/workspace-server/internal/providers/providers.yaml b/workspace-server/internal/providers/providers.yaml index 85b341f33..6754261f2 100644 --- a/workspace-server/internal/providers/providers.yaml +++ b/workspace-server/internal/providers/providers.yaml @@ -627,6 +627,108 @@ providers: model_prefix_match: "^custom[:/]" model_aliases: [] + # =========================================================================== + # DEDICATED BYOK-VENDOR providers (cp#529). These exist so the NAMESPACED + # BYOK ids the hermes/openclaw/codex templates offer for the SHARED upstream + # vendors (anthropic, openai, gemini, minimax, groq) become routable with the + # TENANT's OWN vendor key — WITHOUT routing them through the platform-shared + # `platform` provider (which would bill the platform's key: a money bug). + # + # Each is NON-PLATFORM (name != "platform") -> IsPlatform()==false -> BYOK + # billing: the workspace env supplies the vendor key, never the platform key. + # + # COLLISION-FREE BY CONSTRUCTION: every matcher is NAMESPACED (anchored on the + # `vendor/` slash form or `vendor:` colon form) so it is DISJOINT from the + # platform vendors' BARE matchers (anthropic-api `^claude`, openai-subscription + # `^gpt-`, openai-api `^openai-api[:/]`, minimax `(?i)^minimax-m`, + # google `^gemini-`, minimax-cn `^minimax-cn[:/]`). DeriveProvider's overlap + # guard (no slug may match two native providers) stays green — verified for all + # 20 residual ids (cp#529). + # + # These siblings of the platform/upstream vendor entries point at the SAME + # PUBLIC upstream base URLs, but carry NO upstream_vendor (they are BYOK + # passthroughs, not proxy upstream targets — the proxy never dials a tenant's + # own key) and use the namespaced matchers above instead of the bare proxy + # prefixes. + # =========================================================================== + - name: byok-anthropic + display_name: "Anthropic (BYOK)" + vendor_logo: "anthropic" + protocol: anthropic + auth_mode: anthropic_api + base_url_template: "https://api.anthropic.com/v1" + base_url_anthropic: "https://api.anthropic.com/v1" + auth_env: [ANTHROPIC_API_KEY] + auth_token_env: ANTHROPIC_API_KEY + # Namespaced BYOK form `anthropic/` (hermes). DISJOINT from + # anthropic-api's bare `^claude` and anthropic-oauth's alias set. + model_prefix_match: "^anthropic/" + model_aliases: [] + + - name: byok-openai + display_name: "OpenAI (BYOK)" + vendor_logo: "openai" + protocol: openai + auth_mode: anthropic_api # openai-protocol; auth is a bearer API key. + base_url_template: "https://api.openai.com/v1" + base_url_anthropic: null + auth_env: [OPENAI_API_KEY] + auth_token_env: OPENAI_API_KEY + # Namespaced BYOK forms `openai/` (hermes) + `openai:` + # (openclaw). DISJOINT from openai-subscription's bare `^gpt-` and + # openai-api's `^openai-api[:/]` (the dash after `openai` keeps the two + # apart: `openai:` / `openai/` never start with `openai-api`). + model_prefix_match: "^openai[:/]" + model_aliases: [] + + - name: byok-gemini + display_name: "Google Gemini (BYOK)" + vendor_logo: "google" + protocol: openai + auth_mode: third_party_anthropic_compat + base_url_template: "https://generativelanguage.googleapis.com/v1beta/openai" + base_url_anthropic: null + auth_env: [GEMINI_API_KEY, GOOGLE_API_KEY] + auth_token_env: ANTHROPIC_AUTH_TOKEN + # Namespaced BYOK form `gemini/` (hermes). DISJOINT from the `google` + # vendor's bare `^gemini-` and `vertex`'s `^vertex:`. + model_prefix_match: "^gemini/" + model_aliases: [] + + - name: byok-minimax + display_name: "MiniMax (BYOK)" + vendor_logo: "minimax" + protocol: openai + auth_mode: third_party_anthropic_compat + base_url_template: "https://api.minimax.io/v1" + base_url_anthropic: null + auth_env: [MINIMAX_API_KEY] + auth_token_env: ANTHROPIC_AUTH_TOKEN + # Namespaced BYOK forms `minimax:` (openclaw) + `minimax/` + # (hermes), PLUS the codex-runtime alias `codex-minimax-m2.7` (the codex + # template's `minimax-token-plan` route — same upstream api.minimax.io, + # tenant MINIMAX_API_KEY). The `codex-minimax-` leg is NARROWLY anchored so + # it resolves that one codex id WITHOUT a broad matcher: it is DISJOINT from + # `minimax` (?i)^minimax-m (which needs `minimax-m`, not `codex-`) and from + # `minimax-cn` ^minimax-cn[:/]. Verified collision-free for all 20 residual + # ids + codex-minimax-m2.7 (cp#529). + model_prefix_match: "(?i)^(minimax[:/]|codex-minimax-)" + model_aliases: [] + + - name: groq + display_name: "Groq" + vendor_logo: "groq" + protocol: openai + auth_mode: third_party_anthropic_compat + base_url_template: "https://api.groq.com/openai/v1" + base_url_anthropic: null + auth_env: [GROQ_API_KEY] + auth_token_env: ANTHROPIC_AUTH_TOKEN + # Namespaced BYOK form `groq:` (openclaw). No other provider matches + # the `groq:` prefix. + model_prefix_match: "^groq:" + model_aliases: [] + # ============================================================================= # RUNTIME NATIVE SUPPORT MATRIX (RFC #340 — CTO correction 2026-05-26) # ============================================================================= @@ -792,11 +894,6 @@ runtimes: # bare-vendor providers into its NATIVE prefix-routing set so the BYOK # ids the hermes template offers (openrouter/…, huggingface/…, deepseek/…, # zai:…, etc.) resolve via DeriveProvider. ALL tenant-key (BYOK). - # GUARDRAIL: the platform-shared vendors (openai/gemini/minimax/anthropic - # and groq) are DELIBERATELY ABSENT here — wiring them would route a - # customer model through the platform's key (a money bug); so hermes ids - # like anthropic/claude-*, gemini/*, openai/*, minimax/*, groq:* remain - # unroutable (residual drift) until dedicated BYOK-vendor providers exist. - name: openrouter - name: huggingface - name: ai-gateway @@ -813,6 +910,17 @@ runtimes: - name: zai - name: xiaomi-mimo - name: alibaba + # DEDICATED BYOK-VENDOR arms (cp#529): the namespaced ids hermes offers for + # the SHARED upstream vendors (anthropic/claude-*, gemini/*, openai/*, + # minimax/*) NOW resolve to these tenant-key BYOK-vendor providers — NOT + # the platform-shared `platform` provider (which would bill the platform's + # key). NAME-ONLY (no models) → no platform-menu change, prefix-routing + # only, BYOK-billed. This converts the last 12 hermes residual ids from + # cp#529 drift to routable. + - name: byok-anthropic + - name: byok-gemini + - name: byok-openai + - name: byok-minimax # codex: OpenAI — BYOK split across TWO native providers # (openai-subscription + openai-api), mirroring claude-code's anthropic @@ -864,6 +972,14 @@ runtimes: models: - openai/gpt-5.4 - openai/gpt-5.4-mini + # NAME-ONLY BYOK arm (cp#529): the codex template offers a BYOK MiniMax + # token-plan model `codex-minimax-m2.7` (its `minimax-token-plan` provider: + # base_url api.minimax.io, tenant MINIMAX_API_KEY, model_id_override + # codex-MiniMax-M2.7). It resolves to byok-minimax via the narrowly-anchored + # `codex-minimax-` leg of byok-minimax's matcher (same upstream, tenant key) + # — NOT a broad matcher. NAME-ONLY → no platform-menu change, BYOK-billed. + # Converts the last codex residual id from cp#529 drift to routable. + - name: byok-minimax # openclaw: native Kimi only. openclaw's moonshot: model prefix + a # KIMI_API_KEY (sk-kimi-*) routes to api.kimi.com/coding (kimi-for-coding), @@ -886,11 +1002,17 @@ runtimes: # but wire openclaw's CONFIRMED-NON-PLATFORM passthroughs into its NATIVE # prefix-routing set so the BYOK colon/slash ids the openclaw template # offers (openrouter:…, custom:…) resolve via DeriveProvider. BYOK only. - # GUARDRAIL: the platform-shared openclaw ids openai:*, minimax:*, groq:* - # are DELIBERATELY ABSENT (groq has no provider at all) — they stay - # unroutable residual drift rather than billing the platform's key. - name: openrouter - name: custom + # DEDICATED BYOK-VENDOR arms (cp#529): openclaw's default model is + # `minimax:MiniMax-M2.7`, plus it offers `openai:*` and `groq:*` BYOK ids. + # These NOW resolve to the tenant-key BYOK-vendor providers (NOT the + # platform key). NAME-ONLY → prefix-routing only, BYOK-billed. This converts + # the last 7 openclaw residual ids from cp#529 drift to routable AND makes + # the runtime's DEFAULT model (minimax:MiniMax-M2.7) resolve. + - name: byok-openai + - name: byok-minimax + - name: groq # google-adk: Gemini via Vertex AI, keyless ADC (Workload Identity diff --git a/workspace-server/internal/providers/runtimes_test.go b/workspace-server/internal/providers/runtimes_test.go index f684b00c0..99da2c962 100644 --- a/workspace-server/internal/providers/runtimes_test.go +++ b/workspace-server/internal/providers/runtimes_test.go @@ -38,14 +38,18 @@ var runtimeNativeProviders = map[string][]string{ "hermes": {"kimi-coding", "platform", "openrouter", "huggingface", "ai-gateway", "opencode-zen", "opencode-go", "kilocode", "custom", "nvidia", "arcee", "ollama-cloud", "minimax-cn", - "nousresearch", "deepseek", "zai", "xiaomi-mimo", "alibaba"}, + "nousresearch", "deepseek", "zai", "xiaomi-mimo", "alibaba", + // cp#529 dedicated BYOK-vendor name-only arms (shared-vendor namespaced ids). + "byok-anthropic", "byok-gemini", "byok-openai", "byok-minimax"}, // codex's OpenAI BYOK is split across the OAuth subscription arm // (openai-subscription) and the direct-key arm (openai-api), mirroring // claude-code's anthropic oauth+api split; platform openai via the proxy - // Responses surface. No name-only BYOK arms (its templates offer no - // passthrough ids). - "codex": {"openai-subscription", "openai-api", "platform"}, - "openclaw": {"kimi-coding", "platform", "openrouter", "custom"}, + // Responses surface. cp#529 adds the byok-minimax name-only arm so the + // template's BYOK MiniMax token-plan id (codex-minimax-m2.7) resolves. + "codex": {"openai-subscription", "openai-api", "platform", "byok-minimax"}, + "openclaw": {"kimi-coding", "platform", "openrouter", "custom", + // cp#529 dedicated BYOK-vendor name-only arms (openai:/minimax:/groq:). + "byok-openai", "byok-minimax", "groq"}, } func sortedCopy(in []string) []string { diff --git a/workspace-server/internal/providers/sync_canonical_test.go b/workspace-server/internal/providers/sync_canonical_test.go index 2a1a687da..ab15eff0e 100644 --- a/workspace-server/internal/providers/sync_canonical_test.go +++ b/workspace-server/internal/providers/sync_canonical_test.go @@ -29,7 +29,7 @@ import ( // canonicalProvidersYAMLSHA256 is the sha256 of the canonical providers.yaml as // synced from molecule-controlplane. Bumped deliberately on each re-sync (see // file doc). Cross-checked live by the sync-providers-yaml CI workflow. -const canonicalProvidersYAMLSHA256 = "bd54d8a4b4139175edca1e723496e283e3bb82a5be8da01fd195835338f505db" +const canonicalProvidersYAMLSHA256 = "846ddef11ec423ebf2e96b5da21bd89129dbc3f0a2d14ac086940e005c079387" func TestSyncedYAMLMatchesCanonicalSHA(t *testing.T) { sum := sha256.Sum256(embeddedYAML)