From aeea733cd6aeb97a946cee9aa5a8575a4c5e9637 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Tue, 5 May 2026 16:42:36 +0200 Subject: [PATCH] fix: expose sf-scoped providers --- packages/pi-ai/src/env-api-keys.test.ts | 18 ++++++ packages/pi-ai/src/env-api-keys.ts | 2 +- .../pi-ai/src/web-runtime-env-api-keys.ts | 2 +- packages/pi-coding-agent/src/cli/args.ts | 3 +- .../core/model-registry-proxy-routing.test.ts | 20 +++++++ .../src/core/model-registry.ts | 3 + src/onboarding.ts | 14 +++++ src/resources/extensions/sf/key-manager.js | 11 +++- .../extensions/sf/preferences-models.js | 3 + .../integration/web-mode-runtime-harness.ts | 1 + .../web-onboarding-contract.test.ts | 6 ++ src/web/onboarding-service.ts | 60 +++++++++++++++++++ 12 files changed, 138 insertions(+), 5 deletions(-) diff --git a/packages/pi-ai/src/env-api-keys.test.ts b/packages/pi-ai/src/env-api-keys.test.ts index 760a4f9fe..80f612140 100644 --- a/packages/pi-ai/src/env-api-keys.test.ts +++ b/packages/pi-ai/src/env-api-keys.test.ts @@ -38,4 +38,22 @@ describe("getEnvApiKey", () => { else process.env.GOOGLE_GENERATIVE_AI_API_KEY = savedGoogleGenerative; } }); + + it("uses the OpenCode Go subscription key before the Zen key", () => { + const savedZen = process.env.OPENCODE_API_KEY; + const savedGo = process.env.OPENCODE_GO_API_KEY; + + process.env.OPENCODE_API_KEY = "zen-key"; + process.env.OPENCODE_GO_API_KEY = "go-key"; + + try { + assert.equal(getEnvApiKey("opencode"), "zen-key"); + assert.equal(getEnvApiKey("opencode-go"), "go-key"); + } finally { + if (savedZen === undefined) delete process.env.OPENCODE_API_KEY; + else process.env.OPENCODE_API_KEY = savedZen; + if (savedGo === undefined) delete process.env.OPENCODE_GO_API_KEY; + else process.env.OPENCODE_GO_API_KEY = savedGo; + } + }); }); diff --git a/packages/pi-ai/src/env-api-keys.ts b/packages/pi-ai/src/env-api-keys.ts index be6406e03..a016211fd 100644 --- a/packages/pi-ai/src/env-api-keys.ts +++ b/packages/pi-ai/src/env-api-keys.ts @@ -166,7 +166,7 @@ export function getEnvApiKey(provider: any): string | undefined { "minimax-cn": "MINIMAX_CN_API_KEY", huggingface: "HF_TOKEN", opencode: "OPENCODE_API_KEY", - "opencode-go": "OPENCODE_API_KEY", + "opencode-go": ["OPENCODE_GO_API_KEY", "OPENCODE_API_KEY"], "kimi-coding": "KIMI_API_KEY", xiaomi: "XIAOMI_API_KEY", "xiaomi-token-plan-ams": "XIAOMI_API_KEY", diff --git a/packages/pi-ai/src/web-runtime-env-api-keys.ts b/packages/pi-ai/src/web-runtime-env-api-keys.ts index 8091f2986..950f848ec 100644 --- a/packages/pi-ai/src/web-runtime-env-api-keys.ts +++ b/packages/pi-ai/src/web-runtime-env-api-keys.ts @@ -101,7 +101,7 @@ export function getEnvApiKey(provider: string): string | undefined { "minimax-cn": "MINIMAX_CN_API_KEY", huggingface: "HF_TOKEN", opencode: "OPENCODE_API_KEY", - "opencode-go": "OPENCODE_API_KEY", + "opencode-go": ["OPENCODE_GO_API_KEY", "OPENCODE_API_KEY"], "kimi-coding": "KIMI_API_KEY", xiaomi: "XIAOMI_API_KEY", "xiaomi-token-plan-ams": "XIAOMI_API_KEY", diff --git a/packages/pi-coding-agent/src/cli/args.ts b/packages/pi-coding-agent/src/cli/args.ts index 6c49e8178..80947d95d 100644 --- a/packages/pi-coding-agent/src/cli/args.ts +++ b/packages/pi-coding-agent/src/cli/args.ts @@ -352,7 +352,8 @@ ${chalk.bold("Environment Variables:")} MISTRAL_API_KEY - Mistral API key OLLAMA_API_KEY - Ollama Cloud API key MINIMAX_API_KEY - MiniMax API key - OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key + OPENCODE_API_KEY - OpenCode Zen API key + OPENCODE_GO_API_KEY - OpenCode Go API key KIMI_API_KEY - Kimi For Coding API key AWS_PROFILE - AWS profile for Amazon Bedrock AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock diff --git a/packages/pi-coding-agent/src/core/model-registry-proxy-routing.test.ts b/packages/pi-coding-agent/src/core/model-registry-proxy-routing.test.ts index f665c2fb2..a6fb164f6 100644 --- a/packages/pi-coding-agent/src/core/model-registry-proxy-routing.test.ts +++ b/packages/pi-coding-agent/src/core/model-registry-proxy-routing.test.ts @@ -208,6 +208,26 @@ describe("ModelRegistry.getModelsForProxy — basic", () => { ); }); + it("hides Grok models even when they arrive through aggregators", () => { + const registry = createRegistry(() => true); + const available = registry.getAvailable(); + + assert.ok( + !available.some( + (m) => + m.provider === "xai" || + m.id.toLowerCase().includes("grok") || + m.id.toLowerCase().startsWith("x-ai/"), + ), + "Grok/xAI models should not be visible through direct or aggregate providers", + ); + assert.equal( + registry.find("openrouter", "x-ai/grok-4:free"), + undefined, + "direct lookup should also block OpenRouter Grok SKUs", + ); + }); + it("hides Claude Code because it is not part of the managed provider pool", () => { const registry = createRegistry(() => true); const available = registry.getAvailable(); diff --git a/packages/pi-coding-agent/src/core/model-registry.ts b/packages/pi-coding-agent/src/core/model-registry.ts index e55ed688d..061fb80c2 100644 --- a/packages/pi-coding-agent/src/core/model-registry.ts +++ b/packages/pi-coding-agent/src/core/model-registry.ts @@ -311,6 +311,9 @@ function isModelAllowedByBuiltInProviderPolicy( if (HIDDEN_MODEL_PROVIDERS.has(provider)) { return false; } + if (modelKey.includes("grok") || modelKey.startsWith("x-ai/")) { + return false; + } if (provider === "mistral") { return isMistralSelectionModel(model.id); } diff --git a/src/onboarding.ts b/src/onboarding.ts index c361f01f9..06bfd89e1 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -71,8 +71,11 @@ const LLM_PROVIDER_IDS = [ "google-gemini-cli", "groq", "openrouter", + "zai", "mistral", "xiaomi", + "opencode", + "opencode-go", "ollama", "ollama-cloud", "custom-openai", @@ -91,12 +94,23 @@ const OTHER_PROVIDERS = [ label: "OpenRouter", hint: "200+ models — openrouter.ai/keys", }, + { value: "zai", label: "ZAI", hint: "ZAI GLM — bigmodel.cn" }, { value: "mistral", label: "Mistral", hint: "console.mistral.ai/api-keys" }, { value: "xiaomi", label: "Xiaomi MiMo", hint: "token-plan-ams.xiaomimimo.com", }, + { + value: "opencode", + label: "OpenCode Zen", + hint: "free Zen models — OPENCODE_API_KEY", + }, + { + value: "opencode-go", + label: "OpenCode Go", + hint: "subscription — OPENCODE_GO_API_KEY", + }, { value: "ollama-cloud", label: "Ollama Cloud" }, { value: "custom-openai", diff --git a/src/resources/extensions/sf/key-manager.js b/src/resources/extensions/sf/key-manager.js index 762b244e2..053edc799 100644 --- a/src/resources/extensions/sf/key-manager.js +++ b/src/resources/extensions/sf/key-manager.js @@ -60,6 +60,13 @@ export const PROVIDER_REGISTRY = [ envVar: "MISTRAL_API_KEY", dashboardUrl: "console.mistral.ai", }, + { + id: "zai", + label: "ZAI", + category: "llm", + envVar: "ZAI_API_KEY", + dashboardUrl: "bigmodel.cn", + }, { id: "xiaomi", label: "Xiaomi MiMo", @@ -75,7 +82,7 @@ export const PROVIDER_REGISTRY = [ }, { id: "opencode", - label: "OpenCode", + label: "OpenCode Zen", category: "llm", envVar: "OPENCODE_API_KEY", dashboardUrl: "opencode.ai/zen", @@ -84,7 +91,7 @@ export const PROVIDER_REGISTRY = [ id: "opencode-go", label: "OpenCode Go", category: "llm", - envVar: "OPENCODE_API_KEY", + envVar: "OPENCODE_GO_API_KEY", dashboardUrl: "opencode.ai/zen", }, { diff --git a/src/resources/extensions/sf/preferences-models.js b/src/resources/extensions/sf/preferences-models.js index 97add7993..f1735c6de 100644 --- a/src/resources/extensions/sf/preferences-models.js +++ b/src/resources/extensions/sf/preferences-models.js @@ -99,6 +99,9 @@ function isModelAllowedByBuiltInProviderPolicy(provider, modelId, model) { if (HIDDEN_MODEL_PROVIDERS.has(providerKey)) { return false; } + if (modelKey.includes("grok") || modelKey.startsWith("x-ai/")) { + return false; + } if (providerKey === "mistral") { return isMistralSelectionModel(modelId); } diff --git a/src/tests/integration/web-mode-runtime-harness.ts b/src/tests/integration/web-mode-runtime-harness.ts index 2ff82a3e8..ee3d010cf 100644 --- a/src/tests/integration/web-mode-runtime-harness.ts +++ b/src/tests/integration/web-mode-runtime-harness.ts @@ -57,6 +57,7 @@ const SANITIZED_PROVIDER_ENV_KEYS = [ "MINIMAX_CN_API_KEY", "HF_TOKEN", "OPENCODE_API_KEY", + "OPENCODE_GO_API_KEY", "KIMI_API_KEY", "ALIBABA_API_KEY", "COPILOT_GITHUB_TOKEN", diff --git a/src/tests/integration/web-onboarding-contract.test.ts b/src/tests/integration/web-onboarding-contract.test.ts index 2014c91b8..bcce36ae3 100644 --- a/src/tests/integration/web-onboarding-contract.test.ts +++ b/src/tests/integration/web-onboarding-contract.test.ts @@ -42,6 +42,7 @@ const ONBOARDING_ENV_KEYS = [ "MINIMAX_CN_API_KEY", "HF_TOKEN", "OPENCODE_API_KEY", + "OPENCODE_GO_API_KEY", "KIMI_API_KEY", "ALIBABA_API_KEY", "AWS_PROFILE", @@ -396,6 +397,11 @@ test("boot and onboarding routes expose locked required state plus explicitly sk "groq", "openrouter", "mistral", + "zai", + "xiaomi", + "opencode", + "opencode-go", + "ollama-cloud", ]); const anthropicProvider = bootPayload.onboarding.required.providers.find( (provider: any) => provider.id === "anthropic", diff --git a/src/web/onboarding-service.ts b/src/web/onboarding-service.ts index 342b52334..732aa7723 100644 --- a/src/web/onboarding-service.ts +++ b/src/web/onboarding-service.ts @@ -199,6 +199,31 @@ const REQUIRED_PROVIDER_CATALOG: RequiredProviderCatalogEntry[] = [ supportsApiKey: true, supportsOAuth: false, }, + { id: "zai", label: "ZAI", supportsApiKey: true, supportsOAuth: false }, + { + id: "xiaomi", + label: "Xiaomi MiMo", + supportsApiKey: true, + supportsOAuth: false, + }, + { + id: "opencode", + label: "OpenCode Zen", + supportsApiKey: true, + supportsOAuth: false, + }, + { + id: "opencode-go", + label: "OpenCode Go", + supportsApiKey: true, + supportsOAuth: false, + }, + { + id: "ollama-cloud", + label: "Ollama Cloud", + supportsApiKey: true, + supportsOAuth: false, + }, ]; const OPTIONAL_SECTION_CATALOG: OptionalSectionCatalogEntry[] = [ @@ -460,6 +485,41 @@ async function defaultValidateApiKey( "https://api.mistral.ai/v1/models", apiKey, ); + case "zai": + return await validateBearerRequest( + fetchImpl, + providerId, + "https://open.bigmodel.cn/api/paas/v4/models", + apiKey, + ); + case "xiaomi": + return await validateBearerRequest( + fetchImpl, + providerId, + "https://token-plan-ams.xiaomimimo.com/anthropic/v1/models", + apiKey, + ); + case "opencode": + return await validateBearerRequest( + fetchImpl, + providerId, + "https://opencode.ai/zen/v1/models", + apiKey, + ); + case "opencode-go": + return await validateBearerRequest( + fetchImpl, + providerId, + "https://opencode.ai/zen/go/v1/models", + apiKey, + ); + case "ollama-cloud": + return await validateBearerRequest( + fetchImpl, + providerId, + "https://ollama.com/api/tags", + apiKey, + ); default: return { ok: false,