From cb940115d8a19f925a10726441b8aa0c8f2f6512 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 10:08:34 -0700 Subject: [PATCH 01/19] feat(gateway): add Concentrate AI provider with dynamic model discovery --- .env.example | 15 +++ README.md | 1 + docs/concentrate-setup.md | 45 ++++++++ src/components/ProviderManager.test.tsx | 1 + src/integrations/gateways/concentrate.test.ts | 75 ++++++++++++ src/integrations/gateways/concentrate.ts | 107 ++++++++++++++++++ .../integrationArtifacts.generated.ts | 3 +- .../integrationManifest.generated.ts | 20 ++++ src/integrations/index.test.ts | 1 + 9 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 docs/concentrate-setup.md create mode 100644 src/integrations/gateways/concentrate.test.ts create mode 100644 src/integrations/gateways/concentrate.ts diff --git a/.env.example b/.env.example index 6f982dd2a1..5036c861c7 100644 --- a/.env.example +++ b/.env.example @@ -412,6 +412,21 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here # # Usage limits are displayed via /usage command. + +# ----------------------------------------------------------------------------- +# Option 15: Concentrate AI +# ----------------------------------------------------------------------------- +# Concentrate AI is a unified OpenAI-compatible gateway to 150+ models. +# Get your API key from https://concentrate.ai +# +# CLAUDE_CODE_USE_OPENAI=1 +# CONCENTRATE_API_KEY=sk-cn-your-key-here +# CONCENTRATE_BASE_URL=https://api.concentrate.ai/v1 (optional, default) +# CONCENTRATE_MODEL=deepseek-v4-flash-0731 (optional, default) +# +# Or configure interactively with /provider +# Docs: docs/concentrate-setup.md + # ============================================================================= # OPTIONAL TUNING # ============================================================================= diff --git a/README.md b/README.md index 0996aa2665..eb0a4eebfd 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ Advanced and source-build guides: | OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers | | Z.AI GLM Coding Plan | `/provider` or OpenAI-compatible env vars | Uses `OPENAI_API_KEY` at `https://api.z.ai/api/coding/paas/v4` and defaults to `glm-5.2` | | AI/ML API | `/provider` or `AIMLAPI_API_KEY` ([setup guide](docs/aimlapi-setup.md)) | Uses `https://api.aimlapi.com/v1`, auto-detects the OpenAI-compatible route from `AIMLAPI_API_KEY`, sends OpenClaude attribution headers, and discovers chat-capable models from the public `/models` catalog | +| Concentrate | `/provider` or `CONCENTRATE_API_KEY` ([setup guide](docs/concentrate-setup.md)) | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash-0731` and auto-discovers the chat model catalog | | ApiSmart | `/provider` or `APISMART_API_KEY` | Uses `https://gw.apismart.ai/v1`, defaults to `DEEPSEEK_V4_FLASH`, and supports optional `APISMART_MODEL` plus authenticated model discovery | | Hicap | `/provider` or OpenAI-compatible env vars | Uses `api-key` auth, discovers models from unauthenticated `/models`, and supports Responses mode for `gpt-` models | | Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` | diff --git a/docs/concentrate-setup.md b/docs/concentrate-setup.md new file mode 100644 index 0000000000..6952c6ce2d --- /dev/null +++ b/docs/concentrate-setup.md @@ -0,0 +1,45 @@ +# Concentrate Setup + +OpenClaude connects to [Concentrate AI](https://concentrate.ai) through its OpenAI-compatible endpoint at `https://api.concentrate.ai/v1`. + +## Overview + +Concentrate AI is an aggregating gateway that exposes 150+ chat models behind a single OpenAI-compatible API. OpenClaude ships a first-class `Concentrate` provider preset: it uses `CONCENTRATE_API_KEY`, falls back to `OPENAI_API_KEY`, and auto-discovers the chat model catalog from the public `/v1/models` endpoint. It defaults to `deepseek-v4-flash-0731`. + +## Prerequisites + +- A Concentrate AI account and API key from . +- Concentrate API keys start with `sk-cn`. + +## Interactive setup (`/provider`) + +1. Start OpenClaude and run `/provider`. +2. Choose **Concentrate**. +3. Paste your `CONCENTRATE_API_KEY` (or `OPENAI_API_KEY` if you want to reuse a generic OpenAI-compatible key). +4. The base URL (`https://api.concentrate.ai/v1`) and default model (`deepseek-v4-flash-0731`) are filled in automatically. +5. Switch models any time with `/model`; the picker lists chat-capable models discovered from the Concentrate catalog. + +## Environment variables + +Set the key directly and use the generic OpenAI-compatible route: + +```bash +export CLAUDE_CODE_USE_OPENAI=1 +export CONCENTRATE_API_KEY="sk-cn-your-key-here" +export CONCENTRATE_BASE_URL="https://api.concentrate.ai/v1" # optional +export CONCENTRATE_MODEL="deepseek-v4-flash-0731" # optional +``` + +`OPENAI_API_KEY` also works as a fallback credential for the route. + +## Verify + +- `/status` shows **Concentrate** as the active provider with the `https://api.concentrate.ai/v1` base URL. +- `/model` lists chat-capable models discovered from the Concentrate catalog. +- Send any prompt to confirm responses come back from the selected model. + +## Notes + +- Model discovery uses the public, unauthenticated `GET /v1/models` endpoint. Non-chat models (embeddings, audio, redaction, moderation, etc.) are filtered out automatically. +- Every model keeps its native Concentrate model ID (`deepseek-v4-flash-0731`, `claude-sonnet-5`, `gpt-4o`, etc.) so switching with `/model` works exactly as on the Concentrate dashboard. +- Usage (`/usage`) reporting is not supported for this provider yet. diff --git a/src/components/ProviderManager.test.tsx b/src/components/ProviderManager.test.tsx index ffa5ce0de8..f316add8e3 100644 --- a/src/components/ProviderManager.test.tsx +++ b/src/components/ProviderManager.test.tsx @@ -141,6 +141,7 @@ const PRESET_ORDER = [ 'Bankr', 'ClinePass', 'Cloudflare Workers AI', + 'Concentrate', 'DeepSeek', 'Codex OAuth', 'xAI OAuth (Grok)', diff --git a/src/integrations/gateways/concentrate.test.ts b/src/integrations/gateways/concentrate.test.ts new file mode 100644 index 0000000000..250dc50cec --- /dev/null +++ b/src/integrations/gateways/concentrate.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, test } from 'bun:test' + +import concentrate from './concentrate.js' + +const mapModel = concentrate.catalog?.discovery?.mapModel + +describe('concentrate gateway', () => { + test('uses dynamic discovery and supports fallback auth', () => { + expect(concentrate.id).toBe('concentrate') + expect(concentrate.label).toBe('Concentrate') + expect(concentrate.category).toBe('aggregating') + expect(concentrate.defaultBaseUrl).toBe('https://api.concentrate.ai/v1') + expect(concentrate.defaultModel).toBe('deepseek-v4-flash-0731') + expect(concentrate.supportsModelRouting).toBe(true) + + expect(concentrate.setup.requiresAuth).toBe(true) + expect(concentrate.setup.authMode).toBe('api-key') + expect(concentrate.setup.credentialEnvVars).toEqual(['CONCENTRATE_API_KEY']) + expect(concentrate.setup.dedicatedCredentialsOnly).toBeUndefined() + + expect(concentrate.catalog?.source).toBe('dynamic') + expect(concentrate.catalog?.discovery?.kind).toBe('openai-compatible') + expect(concentrate.catalog?.discovery?.requiresAuth).toBe(false) + + expect(concentrate.validation?.kind).toBe('credential-env') + expect( + (concentrate.validation as { credentialEnvVars: string[] }).credentialEnvVars, + ).toEqual(['CONCENTRATE_API_KEY', 'OPENAI_API_KEY']) + + expect(mapModel).toBeDefined() + }) + + test('mapModel keeps chat models and drops non-chat ids', () => { + if (!mapModel) throw new Error('mapModel missing') + + expect( + mapModel({ + id: 'deepseek-v4-flash-0731', + display_name: 'DeepSeek V4 Flash 0731', + owned_by: 'deepseek', + max_input_tokens: 1_048_576, + max_tokens: 393_216, + }), + ).toEqual({ + id: 'deepseek-v4-flash-0731', + apiName: 'deepseek-v4-flash-0731', + label: 'DeepSeek V4 Flash 0731', + contextWindow: 1_048_576, + maxOutputTokens: 393_216, + }) + + expect( + mapModel({ + id: 'claude-sonnet-5', + display_name: 'Claude Sonnet 5', + owned_by: 'anthropic', + max_input_tokens: 200_000, + max_tokens: 64_000, + }), + ).toEqual({ + id: 'claude-sonnet-5', + apiName: 'claude-sonnet-5', + label: 'Claude Sonnet 5', + contextWindow: 200_000, + maxOutputTokens: 64_000, + }) + + expect(mapModel({ id: 'redact-v1' })).toBeNull() + expect(mapModel({ id: 'gpt-oss-safeguard-120b' })).toBeNull() + expect(mapModel({ id: 'text-embedding-3-small' })).toBeNull() + expect(mapModel({ id: 'whisper-1' })).toBeNull() + expect(mapModel({})).toBeNull() + expect(mapModel(null)).toBeNull() + }) +}) diff --git a/src/integrations/gateways/concentrate.ts b/src/integrations/gateways/concentrate.ts new file mode 100644 index 0000000000..bf4fb261d2 --- /dev/null +++ b/src/integrations/gateways/concentrate.ts @@ -0,0 +1,107 @@ +import { defineGateway } from '../define.js' + +const NON_CHAT_MODEL_PATTERN = + /redact|safeguard|embed|embedding|whisper|tts|dall-e|rerank|moderation|image-generation|video-generation|audio-generation/i + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +function getTrimmedString( + record: Record, + key: string, +): string | undefined { + const value = record[key] + return typeof value === 'string' ? value.trim() : undefined +} + +function getPositiveInteger(value: unknown): number | undefined { + if ( + typeof value === 'number' && + Number.isFinite(value) && + Number.isInteger(value) && + value > 0 + ) { + return value + } + return undefined +} + +function mapConcentrateModel(raw: unknown) { + if (!isRecord(raw)) { + return null + } + + const id = getTrimmedString(raw, 'id') + if (!id || NON_CHAT_MODEL_PATTERN.test(id)) { + return null + } + + const displayName = getTrimmedString(raw, 'display_name') + const label = displayName || id + const contextWindow = getPositiveInteger(raw.max_input_tokens) + const maxOutputTokens = getPositiveInteger(raw.max_tokens) + + return { + id, + apiName: id, + label, + ...(contextWindow !== undefined ? { contextWindow } : {}), + ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), + } +} + +export default defineGateway({ + id: 'concentrate', + label: 'Concentrate', + category: 'aggregating', + defaultBaseUrl: 'https://api.concentrate.ai/v1', + defaultModel: 'deepseek-v4-flash-0731', + supportsModelRouting: true, + setup: { + requiresAuth: true, + authMode: 'api-key', + credentialEnvVars: ['CONCENTRATE_API_KEY'], + }, + startup: { + probeReadiness: 'openai-compatible-models', + }, + transportConfig: { + kind: 'openai-compatible', + openaiShim: { + supportsApiFormatSelection: false, + supportsAuthHeaders: false, + maxTokensField: 'max_tokens', + }, + }, + preset: { + id: 'concentrate', + description: 'Concentrate AI — 150+ models via OpenAI-compatible API', + vendorId: 'openai', + apiKeyEnvVars: ['CONCENTRATE_API_KEY', 'OPENAI_API_KEY'], + baseUrlEnvVars: ['CONCENTRATE_BASE_URL'], + modelEnvVars: ['CONCENTRATE_MODEL', 'OPENAI_MODEL'], + }, + validation: { + kind: 'credential-env', + routing: { + matchDefaultBaseUrl: true, + matchBaseUrlHosts: ['api.concentrate.ai'], + }, + credentialEnvVars: ['CONCENTRATE_API_KEY', 'OPENAI_API_KEY'], + missingCredentialMessage: + 'Concentrate auth is required. Set CONCENTRATE_API_KEY or OPENAI_API_KEY.', + }, + catalog: { + source: 'dynamic', + discovery: { + kind: 'openai-compatible', + requiresAuth: false, + mapModel: mapConcentrateModel, + }, + discoveryCacheTtl: '1d', + discoveryRefreshMode: 'startup', + allowManualRefresh: true, + }, + usage: { supported: false }, +}) diff --git a/src/integrations/generated/integrationArtifacts.generated.ts b/src/integrations/generated/integrationArtifacts.generated.ts index 87a002174e..b4416e27d5 100644 --- a/src/integrations/generated/integrationArtifacts.generated.ts +++ b/src/integrations/generated/integrationArtifacts.generated.ts @@ -27,6 +27,7 @@ import gatewayAzureOpenai from '../gateways/azure-openai.js' import gatewayBedrock from '../gateways/bedrock.js' import gatewayClinepass from '../gateways/clinepass.js' import gatewayCloudflare from '../gateways/cloudflare.js' +import gatewayConcentrate from '../gateways/concentrate.js' import gatewayCustom from '../gateways/custom.js' import gatewayDashscopeCn from '../gateways/dashscope-cn.js' import gatewayDashscopeIntl from '../gateways/dashscope-intl.js' @@ -90,7 +91,7 @@ import modelXai from '../models/xai.js' import modelXiaomiMimo from '../models/xiaomi-mimo.js' export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorBankr, vendorDeepseek, vendorFireworks, vendorGemini, vendorLongcat, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[] -export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[] +export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[] export const ANTHROPIC_PROXY_DESCRIPTORS = [anthropicproxyCustom] as const satisfies readonly AnthropicProxyDescriptor[] export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLing, brandLlama, brandLongcat, brandMacaron, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandTencent, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[] export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelKimi, modelLing, modelLlama, modelLongcat, modelMacaron, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[] diff --git a/src/integrations/generated/integrationManifest.generated.ts b/src/integrations/generated/integrationManifest.generated.ts index 2ad565a0fb..3df2e790d5 100644 --- a/src/integrations/generated/integrationManifest.generated.ts +++ b/src/integrations/generated/integrationManifest.generated.ts @@ -176,6 +176,25 @@ export const PROVIDER_PRESET_MANIFEST = [ "OPENAI_MODEL" ] }, + { + "preset": "concentrate", + "routeKind": "gateway", + "routeId": "concentrate", + "vendorId": "openai", + "gatewayId": "concentrate", + "description": "Concentrate AI — 150+ models via OpenAI-compatible API", + "apiKeyEnvVars": [ + "CONCENTRATE_API_KEY", + "OPENAI_API_KEY" + ], + "baseUrlEnvVars": [ + "CONCENTRATE_BASE_URL" + ], + "modelEnvVars": [ + "CONCENTRATE_MODEL", + "OPENAI_MODEL" + ] + }, { "preset": "deepseek", "routeKind": "vendor", @@ -548,6 +567,7 @@ export const ORDERED_PROVIDER_PRESETS = [ "bankr", "clinepass", "cloudflare", + "concentrate", "deepseek", "fireworks", "gemini", diff --git a/src/integrations/index.test.ts b/src/integrations/index.test.ts index 23fd0c485a..0248f27a96 100644 --- a/src/integrations/index.test.ts +++ b/src/integrations/index.test.ts @@ -88,6 +88,7 @@ describe('loaded registry validation', () => { test('gateway defaultModel values are present unless provided outside curated catalog metadata', () => { const routesWithExternalDefaultModelSources = new Set([ 'atomic-chat', + 'concentrate', 'custom', 'lmstudio', 'ollama', From 16faa995010a4fb3e6e8194951a6a1803c2b9adc Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 10:39:58 -0700 Subject: [PATCH 02/19] fix(concentrate): prompt for Concentrate API key in /provider instead of pre-filling OPENAI_API_KEY --- src/integrations/gateways/concentrate.ts | 2 +- src/integrations/generated/integrationManifest.generated.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/integrations/gateways/concentrate.ts b/src/integrations/gateways/concentrate.ts index bf4fb261d2..208afeda7a 100644 --- a/src/integrations/gateways/concentrate.ts +++ b/src/integrations/gateways/concentrate.ts @@ -78,7 +78,7 @@ export default defineGateway({ id: 'concentrate', description: 'Concentrate AI — 150+ models via OpenAI-compatible API', vendorId: 'openai', - apiKeyEnvVars: ['CONCENTRATE_API_KEY', 'OPENAI_API_KEY'], + apiKeyEnvVars: ['CONCENTRATE_API_KEY'], baseUrlEnvVars: ['CONCENTRATE_BASE_URL'], modelEnvVars: ['CONCENTRATE_MODEL', 'OPENAI_MODEL'], }, diff --git a/src/integrations/generated/integrationManifest.generated.ts b/src/integrations/generated/integrationManifest.generated.ts index 3df2e790d5..818330267a 100644 --- a/src/integrations/generated/integrationManifest.generated.ts +++ b/src/integrations/generated/integrationManifest.generated.ts @@ -184,8 +184,7 @@ export const PROVIDER_PRESET_MANIFEST = [ "gatewayId": "concentrate", "description": "Concentrate AI — 150+ models via OpenAI-compatible API", "apiKeyEnvVars": [ - "CONCENTRATE_API_KEY", - "OPENAI_API_KEY" + "CONCENTRATE_API_KEY" ], "baseUrlEnvVars": [ "CONCENTRATE_BASE_URL" From 3e154c78c1b680af3a8c6621b5e19e227f99db58 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 11:20:39 -0700 Subject: [PATCH 03/19] docs(concentrate): remove standalone setup guide to match other gateways --- .env.example | 1 - README.md | 2 +- docs/concentrate-setup.md | 45 --------------------------------------- 3 files changed, 1 insertion(+), 47 deletions(-) delete mode 100644 docs/concentrate-setup.md diff --git a/.env.example b/.env.example index 5036c861c7..36b2b3be36 100644 --- a/.env.example +++ b/.env.example @@ -425,7 +425,6 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here # CONCENTRATE_MODEL=deepseek-v4-flash-0731 (optional, default) # # Or configure interactively with /provider -# Docs: docs/concentrate-setup.md # ============================================================================= # OPTIONAL TUNING diff --git a/README.md b/README.md index eb0a4eebfd..01187113e8 100644 --- a/README.md +++ b/README.md @@ -290,7 +290,7 @@ Advanced and source-build guides: | OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers | | Z.AI GLM Coding Plan | `/provider` or OpenAI-compatible env vars | Uses `OPENAI_API_KEY` at `https://api.z.ai/api/coding/paas/v4` and defaults to `glm-5.2` | | AI/ML API | `/provider` or `AIMLAPI_API_KEY` ([setup guide](docs/aimlapi-setup.md)) | Uses `https://api.aimlapi.com/v1`, auto-detects the OpenAI-compatible route from `AIMLAPI_API_KEY`, sends OpenClaude attribution headers, and discovers chat-capable models from the public `/models` catalog | -| Concentrate | `/provider` or `CONCENTRATE_API_KEY` ([setup guide](docs/concentrate-setup.md)) | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash-0731` and auto-discovers the chat model catalog | +| Concentrate | `/provider` or `CONCENTRATE_API_KEY` | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash-0731` and auto-discovers the chat model catalog | | ApiSmart | `/provider` or `APISMART_API_KEY` | Uses `https://gw.apismart.ai/v1`, defaults to `DEEPSEEK_V4_FLASH`, and supports optional `APISMART_MODEL` plus authenticated model discovery | | Hicap | `/provider` or OpenAI-compatible env vars | Uses `api-key` auth, discovers models from unauthenticated `/models`, and supports Responses mode for `gpt-` models | | Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` | diff --git a/docs/concentrate-setup.md b/docs/concentrate-setup.md deleted file mode 100644 index 6952c6ce2d..0000000000 --- a/docs/concentrate-setup.md +++ /dev/null @@ -1,45 +0,0 @@ -# Concentrate Setup - -OpenClaude connects to [Concentrate AI](https://concentrate.ai) through its OpenAI-compatible endpoint at `https://api.concentrate.ai/v1`. - -## Overview - -Concentrate AI is an aggregating gateway that exposes 150+ chat models behind a single OpenAI-compatible API. OpenClaude ships a first-class `Concentrate` provider preset: it uses `CONCENTRATE_API_KEY`, falls back to `OPENAI_API_KEY`, and auto-discovers the chat model catalog from the public `/v1/models` endpoint. It defaults to `deepseek-v4-flash-0731`. - -## Prerequisites - -- A Concentrate AI account and API key from . -- Concentrate API keys start with `sk-cn`. - -## Interactive setup (`/provider`) - -1. Start OpenClaude and run `/provider`. -2. Choose **Concentrate**. -3. Paste your `CONCENTRATE_API_KEY` (or `OPENAI_API_KEY` if you want to reuse a generic OpenAI-compatible key). -4. The base URL (`https://api.concentrate.ai/v1`) and default model (`deepseek-v4-flash-0731`) are filled in automatically. -5. Switch models any time with `/model`; the picker lists chat-capable models discovered from the Concentrate catalog. - -## Environment variables - -Set the key directly and use the generic OpenAI-compatible route: - -```bash -export CLAUDE_CODE_USE_OPENAI=1 -export CONCENTRATE_API_KEY="sk-cn-your-key-here" -export CONCENTRATE_BASE_URL="https://api.concentrate.ai/v1" # optional -export CONCENTRATE_MODEL="deepseek-v4-flash-0731" # optional -``` - -`OPENAI_API_KEY` also works as a fallback credential for the route. - -## Verify - -- `/status` shows **Concentrate** as the active provider with the `https://api.concentrate.ai/v1` base URL. -- `/model` lists chat-capable models discovered from the Concentrate catalog. -- Send any prompt to confirm responses come back from the selected model. - -## Notes - -- Model discovery uses the public, unauthenticated `GET /v1/models` endpoint. Non-chat models (embeddings, audio, redaction, moderation, etc.) are filtered out automatically. -- Every model keeps its native Concentrate model ID (`deepseek-v4-flash-0731`, `claude-sonnet-5`, `gpt-4o`, etc.) so switching with `/model` works exactly as on the Concentrate dashboard. -- Usage (`/usage`) reporting is not supported for this provider yet. From a843b37309ea5434bba485038237d7ceb711cae4 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 12:43:37 -0700 Subject: [PATCH 04/19] fix(concentrate): dedicated-credential-only routing, env-only identity, and credential isolation - Make Concentrate dedicatedCredentialsOnly so ambient OPENAI_API_KEY is never forwarded. Only CONCENTRATE_API_KEY authenticates the route. - Resolve Concentrate env-only route identity from CONCENTRATE_API_KEY, CONCENTRATE_BASE_URL, CONCENTRATE_MODEL, or a Concentrate-shaped OPENAI_BASE_URL. - Mirror the dedicated credential into OPENAI_API_KEY only after the route identity is established and only for the canonical /v1 inference endpoint. - Add Concentrate support to --provider concentrate, saved profiles, startup env rebuild, and .env allowlist. - Add regression tests for env-only, flag, saved-profile, and client routing. - Fix adjacent ApiSmart keyless profile leaking string 'undefined' into OPENAI_API_KEY and extend the first providerProfiles test timeout for the now-slower fresh module import. --- src/integrations/gateways/concentrate.test.ts | 4 +- src/integrations/gateways/concentrate.ts | 5 +- src/integrations/routeMetadata.test.ts | 106 ++++++++++++ src/integrations/routeMetadata.ts | 98 ++++++++++- src/services/api/client.test.ts | 120 +++++++++++++ src/services/api/client.ts | 46 +++++ src/utils/envFile.test.ts | 19 +++ src/utils/envFile.ts | 3 + src/utils/providerFlag.test.ts | 95 +++++++++++ src/utils/providerFlag.ts | 71 ++++++-- src/utils/providerProfile.test.ts | 157 ++++++++++++++++++ src/utils/providerProfile.ts | 85 +++++++++- src/utils/providerProfiles.test.ts | 150 ++++++++++++++++- src/utils/providerProfiles.ts | 79 ++++++++- 14 files changed, 1011 insertions(+), 27 deletions(-) diff --git a/src/integrations/gateways/concentrate.test.ts b/src/integrations/gateways/concentrate.test.ts index 250dc50cec..55c9261e20 100644 --- a/src/integrations/gateways/concentrate.test.ts +++ b/src/integrations/gateways/concentrate.test.ts @@ -16,7 +16,7 @@ describe('concentrate gateway', () => { expect(concentrate.setup.requiresAuth).toBe(true) expect(concentrate.setup.authMode).toBe('api-key') expect(concentrate.setup.credentialEnvVars).toEqual(['CONCENTRATE_API_KEY']) - expect(concentrate.setup.dedicatedCredentialsOnly).toBeUndefined() + expect(concentrate.setup.dedicatedCredentialsOnly).toBe(true) expect(concentrate.catalog?.source).toBe('dynamic') expect(concentrate.catalog?.discovery?.kind).toBe('openai-compatible') @@ -25,7 +25,7 @@ describe('concentrate gateway', () => { expect(concentrate.validation?.kind).toBe('credential-env') expect( (concentrate.validation as { credentialEnvVars: string[] }).credentialEnvVars, - ).toEqual(['CONCENTRATE_API_KEY', 'OPENAI_API_KEY']) + ).toEqual(['CONCENTRATE_API_KEY']) expect(mapModel).toBeDefined() }) diff --git a/src/integrations/gateways/concentrate.ts b/src/integrations/gateways/concentrate.ts index 208afeda7a..9522860270 100644 --- a/src/integrations/gateways/concentrate.ts +++ b/src/integrations/gateways/concentrate.ts @@ -62,6 +62,7 @@ export default defineGateway({ requiresAuth: true, authMode: 'api-key', credentialEnvVars: ['CONCENTRATE_API_KEY'], + dedicatedCredentialsOnly: true, }, startup: { probeReadiness: 'openai-compatible-models', @@ -88,9 +89,9 @@ export default defineGateway({ matchDefaultBaseUrl: true, matchBaseUrlHosts: ['api.concentrate.ai'], }, - credentialEnvVars: ['CONCENTRATE_API_KEY', 'OPENAI_API_KEY'], + credentialEnvVars: ['CONCENTRATE_API_KEY'], missingCredentialMessage: - 'Concentrate auth is required. Set CONCENTRATE_API_KEY or OPENAI_API_KEY.', + 'Concentrate auth is required. Set CONCENTRATE_API_KEY.', }, catalog: { source: 'dynamic', diff --git a/src/integrations/routeMetadata.test.ts b/src/integrations/routeMetadata.test.ts index 8ab72799d0..16884f74bf 100644 --- a/src/integrations/routeMetadata.test.ts +++ b/src/integrations/routeMetadata.test.ts @@ -9,6 +9,7 @@ import { isApismartBaseUrl, isCanonicalApismartInferenceBaseUrl, isCloudflareBaseUrl, + isConcentrateBaseUrl, isLongcatBaseUrl, resolveActiveRouteIdFromEnv, resolveRouteCredentialValue, @@ -1009,3 +1010,108 @@ test('getRouteDefaultModel skips hidden and expired catalog entries in the fallb } } }) + +test('isConcentrateBaseUrl matches the Concentrate API host', () => { + expect(isConcentrateBaseUrl('https://api.concentrate.ai/v1')).toBe(true) + expect(isConcentrateBaseUrl('https://api.concentrate.ai/v1/chat/completions')).toBe(true) + expect(isConcentrateBaseUrl('http://api.concentrate.ai/v1')).toBe(false) + expect(isConcentrateBaseUrl('https://api.concentrate.ai:8443/v1')).toBe(false) + expect(isConcentrateBaseUrl('https://api.concentrate.ai.evil.test/v1')).toBe(false) + expect(isConcentrateBaseUrl(undefined)).toBe(false) +}) + +test('resolveActiveRouteIdFromEnv treats Concentrate credential-only env as Concentrate', () => { + expect( + resolveActiveRouteIdFromEnv({ + CONCENTRATE_API_KEY: 'concentrate-key', + }), + ).toBe('concentrate') +}) + +test('resolveActiveRouteIdFromEnv treats CONCENTRATE_BASE_URL as Concentrate intent', () => { + expect( + resolveActiveRouteIdFromEnv({ + CONCENTRATE_API_KEY: 'concentrate-key', + CONCENTRATE_BASE_URL: 'https://api.concentrate.ai/v1', + }), + ).toBe('concentrate') +}) + +test('resolveActiveRouteIdFromEnv treats CONCENTRATE_MODEL as Concentrate intent', () => { + expect( + resolveActiveRouteIdFromEnv({ + CONCENTRATE_API_KEY: 'concentrate-key', + CONCENTRATE_MODEL: 'claude-sonnet-5', + }), + ).toBe('concentrate') +}) + +test('resolveActiveRouteIdFromEnv ignores placeholder Concentrate credentials', () => { + expect( + resolveActiveRouteIdFromEnv({ + CONCENTRATE_API_KEY: 'SUA_CHAVE', + }), + ).not.toBe('concentrate') + expect( + resolveActiveRouteIdFromEnv({ + CONCENTRATE_API_KEY: 'null', + }), + ).not.toBe('concentrate') + expect( + resolveActiveRouteIdFromEnv({ + CONCENTRATE_API_KEY: 'undefined', + }), + ).not.toBe('concentrate') +}) + +test('resolveActiveRouteIdFromEnv prefers Concentrate dedicated key over ambient OpenAI credentials', () => { + expect( + resolveActiveRouteIdFromEnv({ + CONCENTRATE_API_KEY: 'concentrate-key', + OPENAI_API_KEY: 'ambient-openai-key', + OPENAI_API_KEYS: 'ambient-openai-key-a,ambient-openai-key-b', + }), + ).toBe('concentrate') +}) + +test('resolveActiveRouteIdFromEnv does not infer Concentrate with a conflicting OpenAI base URL', () => { + expect( + resolveActiveRouteIdFromEnv({ + CONCENTRATE_API_KEY: 'concentrate-key', + OPENAI_BASE_URL: 'https://api.openai.com/v1', + }), + ).toBe('anthropic') +}) + +test('resolveActiveRouteIdFromEnv keeps an explicit non-OpenAI provider over Concentrate key-only setup', () => { + expect( + resolveActiveRouteIdFromEnv({ + CONCENTRATE_API_KEY: 'concentrate-key', + CLAUDE_CODE_USE_GEMINI: '1', + }), + ).toBe('gemini') +}) + +test('resolveActiveRouteIdFromEnv refines generic OpenAI profile by Concentrate base URL', () => { + expect( + resolveActiveRouteIdFromEnv({ + CLAUDE_CODE_USE_OPENAI: '1', + OPENAI_BASE_URL: 'https://api.concentrate.ai/v1', + }), + ).toBe('concentrate') +}) + +test('getRouteCredentialEnvVars omits generic OpenAI fallback for Concentrate dedicated route', () => { + expect(getRouteCredentialEnvVars('concentrate')).toEqual(['CONCENTRATE_API_KEY']) + expect( + getRouteCredentialValue('concentrate', { + OPENAI_API_KEY: 'generic-openai-key', + }), + ).toBeUndefined() + expect( + getRouteCredentialValue('concentrate', { + OPENAI_API_KEY: 'generic-openai-key', + CONCENTRATE_API_KEY: 'concentrate-key', + }), + ).toBe('concentrate-key') +}) diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index 99260dbf6e..77e787217a 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -230,7 +230,8 @@ function hasUsableEnvCredentialValue( envVar === 'OPENAI_API_KEYS' || envVar === 'OPENAI_API_KEY' || envVar === 'AIMLAPI_API_KEY' || - envVar === 'APISMART_API_KEY' + envVar === 'APISMART_API_KEY' || + envVar === 'CONCENTRATE_API_KEY' ) { return hasUsableOpenAICredential(value) } @@ -418,6 +419,70 @@ export function isClinePassBaseUrl(value: string | undefined): boolean { } } +export function isConcentrateBaseUrl(value: string | undefined): boolean { + const trimmed = value?.trim() + if (!trimmed) { + return false + } + + try { + const url = new URL(trimmed) + return ( + url.protocol === 'https:' && + !url.port && + !url.search && + !url.hash && + url.hostname.toLowerCase() === 'api.concentrate.ai' + ) + } catch { + return false + } +} + +const CONCENTRATE_CANONICAL_INFERENCE_BASE_URL = 'https://api.concentrate.ai/v1' + +export function isCanonicalConcentrateInferenceBaseUrl( + value: string | undefined, +): boolean { + const trimmed = value?.trim() + if (!trimmed) { + return false + } + + try { + const canonical = new URL(CONCENTRATE_CANONICAL_INFERENCE_BASE_URL) + const candidate = new URL(trimmed) + const normalizePath = (pathname: string): string => + pathname.replace(/\/+$/, '') || '/' + return ( + candidate.protocol === 'https:' && + !candidate.port && + !candidate.search && + !candidate.hash && + candidate.hostname.toLowerCase() === canonical.hostname.toLowerCase() && + normalizePath(candidate.pathname) === normalizePath(canonical.pathname) + ) + } catch { + return false + } +} + +export function getConcentrateBaseUrlOverride( + processEnv: NodeJS.ProcessEnv = process.env, +): string | undefined { + const openAIBaseUrl = processEnv.OPENAI_BASE_URL?.trim() + if (isConcentrateBaseUrl(openAIBaseUrl)) { + return openAIBaseUrl + } + + const openAIApiBase = processEnv.OPENAI_API_BASE?.trim() + if (isConcentrateBaseUrl(openAIApiBase)) { + return openAIApiBase + } + + return undefined +} + /** * Host-scoped ApiSmart route match. Used for env-only conflict detection and * base-URL route identity (including `/v1/chat/completions` path suffixes that @@ -886,6 +951,23 @@ export function hasApismartEnvOnlyProviderIntent( ) } +export function hasConcentrateEnvOnlyProviderIntent( + processEnv: NodeJS.ProcessEnv = process.env, +): boolean { + // Concentrate is a dedicated-credentials-only OpenAI-compatible gateway. + // The documented CONCENTRATE_* env vars select the route even when a generic + // OPENAI_API_KEY is present in the shell, so the dedicated credential is the + // only key forwarded to api.concentrate.ai. + return ( + (hasUsableOpenAICredential(processEnv.CONCENTRATE_API_KEY) || + getConcentrateBaseUrlOverride(processEnv) !== undefined || + hasNonEmptyEnvValue(processEnv.CONCENTRATE_BASE_URL) || + hasNonEmptyEnvValue(processEnv.CONCENTRATE_MODEL)) && + !hasConflictingOpenAIBaseUrlForRoute(processEnv, isConcentrateBaseUrl) && + hasNoExplicitNonOpenAIProvider(processEnv) + ) +} + export function resolveEnvOnlyProviderRouteId( processEnv: NodeJS.ProcessEnv = process.env, ): @@ -899,6 +981,7 @@ export function resolveEnvOnlyProviderRouteId( | 'longcat' | 'clinepass' | 'apismart' + | 'concentrate' | null { if ( hasMiniMaxRouteIntent(processEnv) && @@ -947,6 +1030,10 @@ export function resolveEnvOnlyProviderRouteId( return 'apismart' } + if (hasConcentrateEnvOnlyProviderIntent(processEnv)) { + return 'concentrate' + } + return null } @@ -1022,6 +1109,15 @@ export function resolveRouteCredentialValue( ) { return undefined } + // Concentrate is the same: route identity is host-scoped, but the dedicated + // credential is only valid for the documented /v1 inference endpoint. + if ( + routeId === 'concentrate' && + options?.baseUrl !== undefined && + !isCanonicalConcentrateInferenceBaseUrl(options.baseUrl) + ) { + return undefined + } return getRouteCredentialValue(routeId, processEnv) } diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts index b3b921451c..26b200bdb8 100644 --- a/src/services/api/client.test.ts +++ b/src/services/api/client.test.ts @@ -69,6 +69,9 @@ const originalEnv = { LONGCAT_API_KEY: process.env.LONGCAT_API_KEY, AIMLAPI_API_KEY: process.env.AIMLAPI_API_KEY, APISMART_API_KEY: process.env.APISMART_API_KEY, + CONCENTRATE_API_KEY: process.env.CONCENTRATE_API_KEY, + CONCENTRATE_BASE_URL: process.env.CONCENTRATE_BASE_URL, + CONCENTRATE_MODEL: process.env.CONCENTRATE_MODEL, NVIDIA_NIM: process.env.NVIDIA_NIM, NVIDIA_API_KEY: process.env.NVIDIA_API_KEY, ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, @@ -165,6 +168,9 @@ beforeEach(async () => { delete process.env.LONGCAT_API_KEY delete process.env.AIMLAPI_API_KEY delete process.env.APISMART_API_KEY + delete process.env.CONCENTRATE_API_KEY + delete process.env.CONCENTRATE_BASE_URL + delete process.env.CONCENTRATE_MODEL delete process.env.OPENAI_AUTH_HEADER delete process.env.OPENAI_AUTH_SCHEME delete process.env.OPENAI_AUTH_HEADER_VALUE @@ -215,6 +221,9 @@ afterEach(() => { restoreEnv('LONGCAT_API_KEY', originalEnv.LONGCAT_API_KEY) restoreEnv('AIMLAPI_API_KEY', originalEnv.AIMLAPI_API_KEY) restoreEnv('APISMART_API_KEY', originalEnv.APISMART_API_KEY) + restoreEnv('CONCENTRATE_API_KEY', originalEnv.CONCENTRATE_API_KEY) + restoreEnv('CONCENTRATE_BASE_URL', originalEnv.CONCENTRATE_BASE_URL) + restoreEnv('CONCENTRATE_MODEL', originalEnv.CONCENTRATE_MODEL) restoreEnv('NVIDIA_NIM', originalEnv.NVIDIA_NIM) restoreEnv('NVIDIA_API_KEY', originalEnv.NVIDIA_API_KEY) restoreEnv('ANTHROPIC_API_KEY', originalEnv.ANTHROPIC_API_KEY) @@ -771,6 +780,117 @@ test('env-only ApiSmart setup withholds its key from a noncanonical same-host UR expect(process.env.OPENAI_API_KEY).toBeUndefined() }) +test('routes env-only Concentrate requests through the OpenAI-compatible shim', async () => { + let capturedUrl: string | undefined + let capturedHeaders: Headers | undefined + let capturedBody: Record | undefined + + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.GEMINI_API_KEY + delete process.env.GEMINI_MODEL + delete process.env.GEMINI_BASE_URL + delete process.env.GEMINI_AUTH_MODE + process.env.CONCENTRATE_API_KEY = 'concentrate-test-key' + process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/v1' + process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + + globalThis.fetch = (async (input, init) => { + capturedUrl = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : input.url + capturedHeaders = new Headers(init?.headers) + capturedBody = JSON.parse(String(init?.body)) as Record + + return new Response( + JSON.stringify({ + id: 'chatcmpl-concentrate', + model: 'claude-sonnet-5', + choices: [ + { + message: { + role: 'assistant', + content: 'concentrate ok', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 8, + completion_tokens: 3, + total_tokens: 11, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as FetchType + + const client = (await getAnthropicClient({ + maxRetries: 0, + model: 'claude-sonnet-5', + })) as unknown as ShimClient + + const response = await client.beta.messages.create({ + model: 'claude-sonnet-5', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedUrl).toBe('https://api.concentrate.ai/v1/chat/completions') + expect(capturedHeaders?.get('authorization')).toBe('Bearer concentrate-test-key') + expect(capturedBody?.model).toBe('claude-sonnet-5') + expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/v1') + expect(process.env.OPENAI_API_KEY).toBe('concentrate-test-key') + expect(response).toMatchObject({ + role: 'assistant', + model: 'claude-sonnet-5', + }) +}) + +test('env-only Concentrate setup withholds its key from a noncanonical same-host URL', async () => { + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.GEMINI_API_KEY + delete process.env.GEMINI_MODEL + delete process.env.GEMINI_BASE_URL + delete process.env.GEMINI_AUTH_MODE + process.env.CONCENTRATE_API_KEY = 'concentrate-test-key' + process.env.OPENAI_BASE_URL = 'https://api.concentrate.ai/v1/models' + + await getAnthropicClient({ maxRetries: 0, model: 'claude-sonnet-5' }) + + expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1') + expect(process.env.OPENAI_BASE_URL).toBe( + 'https://api.concentrate.ai/v1/models', + ) + expect(process.env.OPENAI_API_KEY).toBeUndefined() +}) + +test('env-only Concentrate does not forward an ambient generic OpenAI key', async () => { + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.GEMINI_API_KEY + delete process.env.GEMINI_MODEL + delete process.env.GEMINI_BASE_URL + delete process.env.GEMINI_AUTH_MODE + process.env.OPENAI_API_KEY = 'generic-openai-key' + process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/v1' + process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + + await getAnthropicClient({ maxRetries: 0, model: 'claude-sonnet-5' }) + + expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1') + expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/v1') + expect(process.env.OPENAI_MODEL).toBe('claude-sonnet-5') + expect(process.env.OPENAI_API_KEY).toBeUndefined() +}) + test('routes env-only AI/ML API requests through the OpenAI-compatible shim despite an ambient OpenAI key', async () => { let capturedUrl: string | undefined let capturedHeaders: Headers | undefined diff --git a/src/services/api/client.ts b/src/services/api/client.ts index 3b6b92f039..800018db2c 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -44,6 +44,7 @@ import { getMiniMaxBaseUrlOverride, getNearaiBaseUrlOverride, isCanonicalApismartInferenceBaseUrl, + isCanonicalConcentrateInferenceBaseUrl, getRouteDefaultBaseUrl, getRouteDefaultModel, getXaiBaseUrlOverride, @@ -411,6 +412,45 @@ function applyApismartEnvOnlyDefaults(): void { delete process.env.ANTHROPIC_CUSTOM_HEADERS } +function applyConcentrateEnvOnlyDefaults(): void { + const baseUrlOverride = + usableProviderConfigEnvValue(process.env.CONCENTRATE_BASE_URL) || + usableProviderConfigEnvValue(process.env.OPENAI_BASE_URL) || + usableProviderConfigEnvValue(process.env.OPENAI_API_BASE) || + undefined + const modelOverride = + usableProviderConfigEnvValue(process.env.CONCENTRATE_MODEL) || + usableProviderConfigEnvValue(process.env.OPENAI_MODEL) || + undefined + const apiKey = process.env.CONCENTRATE_API_KEY + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = + baseUrlOverride ?? getRouteDefaultBaseUrl('concentrate') + process.env.OPENAI_MODEL = modelOverride ?? getRouteDefaultModel('concentrate') + // Concentrate is dedicatedCredentialsOnly: only CONCENTRATE_API_KEY may + // authenticate this route. Mirror it into OPENAI_API_KEY for the shared + // OpenAI-compatible transport after the route identity is established, and + // clear any stale generic key so ambient OpenAI credentials are not + // forwarded to Concentrate. The dedicated key belongs only to Concentrate's + // documented /v1 inference endpoint, so a noncanonical same-host URL withholds + // the credential the same way ApiSmart does. + if ( + hasUsableOpenAICredential(apiKey) && + isCanonicalConcentrateInferenceBaseUrl(process.env.OPENAI_BASE_URL) + ) { + process.env.OPENAI_API_KEY = apiKey + } else { + delete process.env.OPENAI_API_KEY + } + delete process.env.OPENAI_API_FORMAT + delete process.env.OPENAI_AZURE_STYLE + delete process.env.OPENAI_AUTH_HEADER + delete process.env.OPENAI_AUTH_SCHEME + delete process.env.OPENAI_AUTH_HEADER_VALUE + delete process.env.ANTHROPIC_CUSTOM_HEADERS +} + function usableProviderConfigEnvValue( value: string | undefined, ): string | undefined { @@ -561,6 +601,8 @@ export async function getAnthropicClient({ envOnlyProviderRouteId === 'aimlapi' && !useMiniMaxEnvOnlyProvider const useApismartEnvOnlyProvider = envOnlyProviderRouteId === 'apismart' && !useMiniMaxEnvOnlyProvider + const useConcentrateEnvOnlyProvider = + envOnlyProviderRouteId === 'concentrate' && !useMiniMaxEnvOnlyProvider if (useMiniMaxEnvOnlyProvider) { applyMiniMaxEnvOnlyDefaults(model) } @@ -585,6 +627,9 @@ export async function getAnthropicClient({ if (useApismartEnvOnlyProvider) { applyApismartEnvOnlyDefaults() } + if (useConcentrateEnvOnlyProvider) { + applyConcentrateEnvOnlyDefaults() + } const apiProvider = getAPIProvider() const isFirstPartyBaseUrl = isFirstPartyAnthropicBaseUrl() @@ -690,6 +735,7 @@ export async function getAnthropicClient({ useFireworksEnvOnlyProvider || useAimlapiEnvOnlyProvider || useApismartEnvOnlyProvider || + useConcentrateEnvOnlyProvider || isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) || isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) || diff --git a/src/utils/envFile.test.ts b/src/utils/envFile.test.ts index 97847cf947..b257ae3fb6 100644 --- a/src/utils/envFile.test.ts +++ b/src/utils/envFile.test.ts @@ -292,6 +292,25 @@ describe('loadEnvFile', () => { }) }) + it('loads documented Concentrate env-only provider setup values', () => { + const filePath = writeTempEnvFile([ + 'CONCENTRATE_API_KEY=concentrate-key', + 'CONCENTRATE_BASE_URL=https://api.concentrate.ai/v1', + 'CONCENTRATE_MODEL=claude-sonnet-5', + ].join('\n')) + + const loaded = loadEnvFile(filePath) + + expect(process.env.CONCENTRATE_API_KEY).toBe('concentrate-key') + expect(process.env.CONCENTRATE_BASE_URL).toBe('https://api.concentrate.ai/v1') + expect(process.env.CONCENTRATE_MODEL).toBe('claude-sonnet-5') + expect(loaded).toEqual({ + CONCENTRATE_API_KEY: 'concentrate-key', + CONCENTRATE_BASE_URL: 'https://api.concentrate.ai/v1', + CONCENTRATE_MODEL: 'claude-sonnet-5', + }) + }) + it('loads documented Azure OpenAI API version values', () => { const filePath = writeTempEnvFile( 'AZURE_OPENAI_API_VERSION=2024-12-01-preview', diff --git a/src/utils/envFile.ts b/src/utils/envFile.ts index a7093b48d3..f20456b748 100644 --- a/src/utils/envFile.ts +++ b/src/utils/envFile.ts @@ -49,6 +49,9 @@ const ALLOWED_ENV_FILE_KEYS = new Set([ 'CODEX_AUTH_JSON_PATH', 'CODEX_CREDENTIAL_SOURCE', 'CODEX_HOME', + 'CONCENTRATE_API_KEY', + 'CONCENTRATE_BASE_URL', + 'CONCENTRATE_MODEL', 'DASHSCOPE_API_KEY', 'DEEPSEEK_API_KEY', 'EXA_API_KEY', diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index f027f6476d..8c874ac793 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -42,6 +42,9 @@ const ENV_KEYS = [ 'ATLAS_CLOUD_API_KEY', 'APISMART_API_KEY', 'APISMART_MODEL', + 'CONCENTRATE_API_KEY', + 'CONCENTRATE_BASE_URL', + 'CONCENTRATE_MODEL', 'LONGCAT_API_KEY', 'OPENGATEWAY_API_KEY', 'OPENGATEWAY_BASE_URL', @@ -93,6 +96,9 @@ const RESET_KEYS = [ 'ATLAS_CLOUD_API_KEY', 'APISMART_API_KEY', 'APISMART_MODEL', + 'CONCENTRATE_API_KEY', + 'CONCENTRATE_BASE_URL', + 'CONCENTRATE_MODEL', 'LONGCAT_API_KEY', 'OPENGATEWAY_API_KEY', 'OPENGATEWAY_BASE_URL', @@ -1309,3 +1315,92 @@ describe('applyModelFlagFromArgs', () => { expect(process.env.OPENAI_MODEL).toBe('qwen2.5-coder:14b') }) }) + +describe('applyProviderFlag - concentrate', () => { + test('sets Concentrate OpenAI-compatible defaults and mirrors CONCENTRATE_API_KEY', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' + + const result = applyProviderFlag('concentrate', []) + + expect(result.error).toBeUndefined() + expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1') + expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/v1') + expect(process.env.OPENAI_MODEL).toBe('deepseek-v4-flash-0731') + expect(process.env.OPENAI_API_KEY).toBe('concentrate-secret-key') + }) + + test('uses CONCENTRATE_BASE_URL and CONCENTRATE_MODEL from env', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' + process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/v1' + process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + + applyProviderFlag('concentrate', []) + + expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/v1') + expect(process.env.OPENAI_MODEL).toBe('claude-sonnet-5') + }) + + test('explicit --model overrides CONCENTRATE_MODEL and OPENAI_MODEL', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' + process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + + applyProviderFlag('concentrate', ['--model', 'deepseek-v4-pro-0731']) + + expect(process.env.OPENAI_MODEL).toBe('deepseek-v4-pro-0731') + }) + + test('dedicated key overrides a lingering OPENAI_API_KEY from another provider', () => { + process.env.OPENAI_API_KEY = 'existing-openai-key' + process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' + + applyProviderFlag('concentrate', []) + + expect(process.env.OPENAI_API_KEY).toBe('concentrate-secret-key') + }) + + test('clears a stale OPENAI_API_KEY when no Concentrate key is set', () => { + delete process.env.CONCENTRATE_API_KEY + process.env.OPENAI_API_KEY = 'existing-openai-key' + + applyProviderFlag('concentrate', []) + + expect(process.env.OPENAI_API_KEY).toBeUndefined() + }) + + test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])( + 'does not mirror placeholder Concentrate credential %s', + placeholder => { + process.env.CONCENTRATE_API_KEY = placeholder + + applyProviderFlag('concentrate', []) + + expect(process.env.OPENAI_API_KEY).toBeUndefined() + }, + ) + + test('clears a copied Concentrate key from OPENAI_API_KEY when switching to another provider', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' + process.env.OPENAI_API_KEY = 'concentrate-secret-key' + + applyProviderFlag('openai', []) + + expect(process.env.OPENAI_API_KEY).toBeUndefined() + }) + + test('clears unsupported OpenAI shim settings from a previous route', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' + process.env.OPENAI_API_FORMAT = 'responses' + process.env.OPENAI_AUTH_HEADER = 'x-api-key' + process.env.OPENAI_AUTH_SCHEME = 'raw' + process.env.OPENAI_AUTH_HEADER_VALUE = 'stale-value' + process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Proxy-Auth: proxy-secret' + + applyProviderFlag('concentrate', []) + + expect(process.env.OPENAI_API_FORMAT).toBeUndefined() + expect(process.env.OPENAI_AUTH_HEADER).toBeUndefined() + expect(process.env.OPENAI_AUTH_SCHEME).toBeUndefined() + expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBeUndefined() + expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBeUndefined() + }) +}) diff --git a/src/utils/providerFlag.ts b/src/utils/providerFlag.ts index 11264f82f3..22ecb8bc44 100644 --- a/src/utils/providerFlag.ts +++ b/src/utils/providerFlag.ts @@ -350,18 +350,21 @@ export function applyProviderFlag( : process.env.OPENAI_API_KEY !== undefined && process.env.OPENAI_API_KEY === process.env.ATLAS_CLOUD_API_KEY ? 'atlas-cloud' - : process.env.OPENAI_API_KEY !== undefined && - process.env.OPENAI_API_KEY === process.env.APISMART_API_KEY - ? 'apismart' - : process.env.OPENAI_API_KEY !== undefined && - process.env.OPENAI_API_KEY === process.env.NEARAI_API_KEY - ? 'nearai' : process.env.OPENAI_API_KEY !== undefined && - process.env.OPENAI_API_KEY === process.env.FIREWORKS_API_KEY - ? 'fireworks' - : process.env.OPENAI_API_KEY !== undefined && - process.env.OPENAI_API_KEY === process.env.LONGCAT_API_KEY - ? 'longcat' + process.env.OPENAI_API_KEY === process.env.APISMART_API_KEY + ? 'apismart' + : process.env.OPENAI_API_KEY !== undefined && + process.env.OPENAI_API_KEY === process.env.CONCENTRATE_API_KEY + ? 'concentrate' + : process.env.OPENAI_API_KEY !== undefined && + process.env.OPENAI_API_KEY === process.env.NEARAI_API_KEY + ? 'nearai' + : process.env.OPENAI_API_KEY !== undefined && + process.env.OPENAI_API_KEY === process.env.FIREWORKS_API_KEY + ? 'fireworks' + : process.env.OPENAI_API_KEY !== undefined && + process.env.OPENAI_API_KEY === process.env.LONGCAT_API_KEY + ? 'longcat' : process.env.OPENAI_API_KEY !== undefined && opengatewayApiKey !== undefined && opengatewayApiKey.length > 0 && @@ -670,6 +673,52 @@ export function applyProviderFlag( } break + case 'concentrate': + process.env.CLAUDE_CODE_USE_OPENAI = '1' + // Concentrate uses the standard OpenAI-compatible wire contract with no + // alternate API formats or custom auth headers. + clearUnsupportedOpenAIShimSettings('concentrate') + delete process.env.ANTHROPIC_CUSTOM_HEADERS + { + const baseUrlOverride = usableProviderModelEnvValue( + process.env.CONCENTRATE_BASE_URL, + ) + if (baseUrlOverride) { + process.env.OPENAI_BASE_URL = baseUrlOverride + } else { + applyOpenAIBaseUrlDefault( + provider, + defaultBaseUrl ?? 'https://api.concentrate.ai/v1', + ) + } + } + { + const concentrateModel = usableProviderModelEnvValue( + process.env.CONCENTRATE_MODEL, + ) + if (concentrateModel) { + process.env.OPENAI_MODEL = concentrateModel + } else { + process.env.OPENAI_MODEL ??= + usableProviderModelEnvValue(process.env.OPENAI_MODEL) || + defaultModel || + 'deepseek-v4-flash-0731' + } + } + if (model) { + process.env.OPENAI_MODEL = model + } + // DedicatedCredentialsOnly: only CONCENTRATE_API_KEY authenticates this + // route. Mirror it into OPENAI_API_KEY for the shared shim transport and + // clear any stale generic key so ambient OpenAI credentials are never + // forwarded to Concentrate. + if (hasUsableOpenAICredential(process.env.CONCENTRATE_API_KEY)) { + process.env.OPENAI_API_KEY = process.env.CONCENTRATE_API_KEY + } else { + delete process.env.OPENAI_API_KEY + } + break + case 'fireworks': process.env.CLAUDE_CODE_USE_OPENAI = '1' applyOpenAIBaseUrlDefault(provider, defaultBaseUrl) diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index e9fcc004cf..785c165441 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -18,6 +18,7 @@ import { buildAtomicChatProfileEnv, buildApismartProfileEnv, buildCompatibilityProcessEnv, + buildConcentrateProfileEnv, buildCodexProfileEnv, buildGeminiProfileEnv, buildLaunchEnv, @@ -3012,3 +3013,159 @@ test('atomic-chat launch ignores mismatched persisted openai env', async () => { assert.equal(env.CODEX_API_KEY, undefined) assert.equal(env.CHATGPT_ACCOUNT_ID, undefined) }) + +test('openai launch preserves persisted Concentrate dedicated credentials across restart', async () => { + const env = await buildLaunchEnv({ + profile: 'openai', + persisted: profile('openai', { + OPENAI_BASE_URL: 'https://api.concentrate.ai/v1', + OPENAI_MODEL: 'deepseek-v4-flash-0731', + OPENAI_API_KEY: 'concentrate-secret-key', + CONCENTRATE_API_KEY: 'concentrate-secret-key', + }), + goal: 'coding', + processEnv: {}, + }) + + assert.equal(env.OPENAI_BASE_URL, 'https://api.concentrate.ai/v1') + assert.equal(env.OPENAI_MODEL, 'deepseek-v4-flash-0731') + assert.equal(env.OPENAI_API_KEY, 'concentrate-secret-key') + assert.equal(env.CONCENTRATE_API_KEY, 'concentrate-secret-key') + assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'concentrate') +}) + +test('openai launch backfills CONCENTRATE_API_KEY from a legacy OpenAI-shaped Concentrate profile', async () => { + const env = await buildLaunchEnv({ + profile: 'openai', + persisted: profile('openai', { + OPENAI_BASE_URL: 'https://api.concentrate.ai/v1', + OPENAI_MODEL: 'deepseek-v4-flash-0731', + OPENAI_API_KEY: 'concentrate-secret-key', + }), + goal: 'coding', + processEnv: {}, + }) + + assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'concentrate') + assert.equal(env.OPENAI_API_KEY, 'concentrate-secret-key') + assert.equal(env.CONCENTRATE_API_KEY, 'concentrate-secret-key') + assert.equal( + resolveRouteCredentialValue({ + routeId: 'concentrate', + processEnv: env, + baseUrl: env.OPENAI_BASE_URL, + }), + 'concentrate-secret-key', + ) +}) + +test('openai launch never promotes an ambient generic key to a Concentrate credential', async () => { + const env = await buildLaunchEnv({ + profile: 'openai', + persisted: profile('openai', { + OPENAI_BASE_URL: 'https://api.concentrate.ai/v1', + OPENAI_MODEL: 'deepseek-v4-flash-0731', + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'concentrate', + }), + goal: 'coding', + processEnv: { + OPENAI_API_KEY: 'generic-openai-key', + }, + }) + + assert.equal(env.CONCENTRATE_API_KEY, undefined) + assert.equal( + resolveRouteCredentialValue({ + routeId: 'concentrate', + processEnv: env, + baseUrl: env.OPENAI_BASE_URL, + }), + undefined, + ) +}) + +test('openai launch withholds ambient Concentrate credentials from a keyless proxy profile on restart', async () => { + const env = await buildLaunchEnv({ + profile: 'openai', + persisted: profile('openai', { + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'concentrate', + OPENAI_BASE_URL: 'https://proxy.example.com/v1', + OPENAI_MODEL: 'deepseek-v4-flash-0731', + }), + goal: 'coding', + processEnv: { + OPENAI_BASE_URL: 'https://proxy.example.com/v1', + OPENAI_API_KEY: 'ambient-concentrate-key', + CONCENTRATE_API_KEY: 'ambient-concentrate-key', + }, + }) + + assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'concentrate') + assert.equal(env.OPENAI_API_KEY, undefined) + assert.equal(env.CONCENTRATE_API_KEY, undefined) + + const canonical = await buildLaunchEnv({ + profile: 'openai', + persisted: profile('openai', { + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'concentrate', + OPENAI_BASE_URL: 'https://api.concentrate.ai/v1', + OPENAI_MODEL: 'deepseek-v4-flash-0731', + }), + goal: 'coding', + processEnv: { + OPENAI_BASE_URL: 'https://api.concentrate.ai/v1', + OPENAI_API_KEY: 'ambient-concentrate-key', + CONCENTRATE_API_KEY: 'ambient-concentrate-key', + }, + }) + assert.equal(canonical.OPENAI_API_KEY, 'ambient-concentrate-key') + assert.equal(canonical.CONCENTRATE_API_KEY, 'ambient-concentrate-key') +}) + +test('buildStartupEnvFromProfile preserves Concentrate env-only setup over a saved profile', async () => { + const env = await buildStartupEnvFromProfile({ + persisted: profile('openai', { + OPENAI_BASE_URL: 'https://api.openai.com/v1', + OPENAI_MODEL: 'gpt-4o', + OPENAI_API_KEY: 'sk-persisted', + }), + goal: 'balanced', + processEnv: { + CONCENTRATE_API_KEY: 'concentrate-env-only-key', + CONCENTRATE_BASE_URL: 'https://api.concentrate.ai/v1', + CONCENTRATE_MODEL: 'claude-sonnet-5', + }, + }) + + assert.equal(env.CONCENTRATE_API_KEY, 'concentrate-env-only-key') + assert.equal(env.CONCENTRATE_BASE_URL, 'https://api.concentrate.ai/v1') + assert.equal(env.CONCENTRATE_MODEL, 'claude-sonnet-5') + assert.equal(env.OPENAI_API_KEY, undefined) + assert.equal(resolveActiveRouteIdFromEnv(env), 'concentrate') +}) + +test('buildConcentrateProfileEnv prefers CONCENTRATE_MODEL over OPENAI_MODEL', () => { + const env = buildConcentrateProfileEnv({ + apiKey: 'concentrate-secret-key', + processEnv: { + CONCENTRATE_MODEL: 'claude-sonnet-5', + OPENAI_MODEL: 'gpt-4o', + }, + }) + + assert.ok(env) + assert.equal(env?.OPENAI_MODEL, 'claude-sonnet-5') + assert.equal(env?.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'concentrate') +}) + +test('buildConcentrateProfileEnv reads CONCENTRATE_BASE_URL override', () => { + const env = buildConcentrateProfileEnv({ + apiKey: 'concentrate-secret-key', + processEnv: { + CONCENTRATE_BASE_URL: 'https://api.concentrate.ai/v1', + }, + }) + + assert.ok(env) + assert.equal(env?.OPENAI_BASE_URL, 'https://api.concentrate.ai/v1') +}) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 8891e0d6cc..2df498463e 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -26,6 +26,7 @@ import { getRouteDefaultBaseUrl, getRouteDefaultModel, isCanonicalApismartInferenceBaseUrl, + isConcentrateBaseUrl, isLongcatBaseUrl, normalizeXiaomiMimoBaseUrl, resolveRouteCredentialValue, @@ -117,6 +118,9 @@ const PROFILE_ENV_KEYS = [ 'NEARAI_API_KEY', 'FIREWORKS_API_KEY', 'LONGCAT_API_KEY', + 'CONCENTRATE_API_KEY', + 'CONCENTRATE_BASE_URL', + 'CONCENTRATE_MODEL', 'CLINE_API_KEY', 'OPENCODE_API_KEY', 'CLAUDE_CODE_PROVIDER_ROUTE_ID', @@ -204,6 +208,9 @@ export type ProfileEnv = { NEARAI_API_KEY?: string FIREWORKS_API_KEY?: string LONGCAT_API_KEY?: string + CONCENTRATE_API_KEY?: string + CONCENTRATE_BASE_URL?: string + CONCENTRATE_MODEL?: string OPENCODE_API_KEY?: string CLOUDFLARE_API_TOKEN?: string CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS?: string @@ -689,6 +696,51 @@ export function buildApismartProfileEnv(options: { } } +export function buildConcentrateProfileEnv(options: { + model?: string | null + baseUrl?: string | null + apiKey?: string | null + processEnv?: NodeJS.ProcessEnv +}): ProfileEnv | null { + const processEnv = options.processEnv ?? process.env + const key = sanitizeApiKey(options.apiKey ?? processEnv.CONCENTRATE_API_KEY) + if (!key) { + return null + } + + const defaultBaseUrl = getRouteDefaultBaseUrl('concentrate') + const defaultModel = getRouteDefaultModel('concentrate') + if (!defaultBaseUrl || !defaultModel) { + throw new Error('Concentrate route defaults are missing from integration metadata.') + } + const secretSource: SecretValueSource = { + OPENAI_API_KEY: key, + CONCENTRATE_API_KEY: key, + } + const configuredBaseUrl = + sanitizeProviderConfigValue(options.baseUrl, secretSource) || + sanitizeProviderConfigValue(processEnv.CONCENTRATE_BASE_URL, secretSource) || + sanitizeProviderConfigValue(processEnv.OPENAI_BASE_URL, secretSource) + + return { + OPENAI_BASE_URL: configuredBaseUrl || defaultBaseUrl, + OPENAI_MODEL: + normalizeProfileModel( + sanitizeProviderConfigValue(options.model, secretSource), + ) || + normalizeProfileModel( + sanitizeProviderConfigValue(processEnv.CONCENTRATE_MODEL, secretSource), + ) || + normalizeProfileModel( + sanitizeProviderConfigValue(processEnv.OPENAI_MODEL, secretSource), + ) || + defaultModel, + OPENAI_API_KEY: key, + CONCENTRATE_API_KEY: key, + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'concentrate', + } +} + export function buildGeminiProfileEnv(options: { model?: string | null baseUrl?: string | null @@ -932,6 +984,9 @@ export function buildOpenAIProfileEnv(options: { ...(resolveRouteIdFromBaseUrl(resolvedBaseUrl) === 'aimlapi' && key ? { AIMLAPI_API_KEY: key } : {}), + ...(resolveRouteIdFromBaseUrl(resolvedBaseUrl) === 'concentrate' && key + ? { CONCENTRATE_API_KEY: key } + : {}), OPENAI_BASE_URL: resolvedBaseUrl, OPENAI_MODEL: normalizedModel, ...(options.apiFormat ? { OPENAI_API_FORMAT: options.apiFormat } : {}), @@ -1411,7 +1466,8 @@ function hasConcreteProviderSelection( sanitizeApiKey(processEnv.APISMART_API_KEY) !== undefined || sanitizeApiKey(processEnv.FIREWORKS_API_KEY) !== undefined || sanitizeApiKey(processEnv.NEARAI_API_KEY) !== undefined || - sanitizeApiKey(processEnv.LONGCAT_API_KEY) !== undefined + sanitizeApiKey(processEnv.LONGCAT_API_KEY) !== undefined || + sanitizeApiKey(processEnv.CONCENTRATE_API_KEY) !== undefined ) } @@ -2080,8 +2136,12 @@ export async function buildLaunchEnv(options: { effectiveOpenAIRouteId === 'apismart' && !!env.OPENAI_BASE_URL?.trim() && !isCanonicalApismartInferenceBaseUrl(env.OPENAI_BASE_URL) + const isNoncanonicalConcentrateLaunch = + effectiveOpenAIRouteId === 'concentrate' && + !!env.OPENAI_BASE_URL?.trim() && + !isConcentrateBaseUrl(env.OPENAI_BASE_URL) const isNoncanonicalDedicatedOpenAILaunch = - isNoncanonicalAimlapiLaunch || isNoncanonicalApismartLaunch + isNoncanonicalAimlapiLaunch || isNoncanonicalApismartLaunch || isNoncanonicalConcentrateLaunch if (isNoncanonicalDedicatedOpenAILaunch) { delete env.OPENAI_API_KEY delete env.OPENAI_API_KEYS @@ -2115,6 +2175,7 @@ export async function buildLaunchEnv(options: { for (const dedicatedKey of [ 'ATLAS_CLOUD_API_KEY', 'APISMART_API_KEY', + 'CONCENTRATE_API_KEY', 'NEARAI_API_KEY', 'FIREWORKS_API_KEY', 'LONGCAT_API_KEY', @@ -2133,6 +2194,9 @@ export async function buildLaunchEnv(options: { if (dedicatedKey === 'APISMART_API_KEY' && effectiveOpenAIRouteId !== 'apismart') { continue } + if (dedicatedKey === 'CONCENTRATE_API_KEY' && effectiveOpenAIRouteId !== 'concentrate') { + continue + } if (dedicatedKey === 'NVIDIA_API_KEY' && effectiveOpenAIRouteId !== 'nvidia-nim') { continue } @@ -2156,8 +2220,12 @@ export async function buildLaunchEnv(options: { dedicatedKey === 'APISMART_API_KEY' && !!dedicatedBaseUrl && !isCanonicalApismartInferenceBaseUrl(dedicatedBaseUrl) + const withholdAmbientConcentrateKey = + dedicatedKey === 'CONCENTRATE_API_KEY' && + !!dedicatedBaseUrl && + !isConcentrateBaseUrl(dedicatedBaseUrl) const withholdAmbientDedicatedKey = - withholdAmbientAimlapiKey || withholdAmbientApismartKey + withholdAmbientAimlapiKey || withholdAmbientApismartKey || withholdAmbientConcentrateKey // AIMLAPI accepts generic OpenAI credentials, but ApiSmart is // dedicatedCredentialsOnly. Never promote a shell OPENAI_API_KEY into the // dedicated credential on relaunch. @@ -2179,12 +2247,21 @@ export async function buildLaunchEnv(options: { persistedOpenAICredential?.kind === 'usable' ? sanitizeApiKey(persistedOpenAICredential.value) : undefined + const backfillLegacyConcentrateProfileKey = + dedicatedKey === 'CONCENTRATE_API_KEY' && + effectiveOpenAIRouteId === 'concentrate' && + !!dedicatedBaseUrl && + isConcentrateBaseUrl(dedicatedBaseUrl) && + persistedOpenAICredential?.kind === 'usable' + ? sanitizeApiKey(persistedOpenAICredential.value) + : undefined const dedicatedValue = withholdAmbientDedicatedKey ? sanitizeApiKey(persistedEnv[dedicatedKey]) : backfillDedicatedFromOpenAI || sanitizeApiKey(processEnv[dedicatedKey]) || sanitizeApiKey(persistedEnv[dedicatedKey]) || - backfillLegacyApismartProfileKey + backfillLegacyApismartProfileKey || + backfillLegacyConcentrateProfileKey if (dedicatedValue) { env[dedicatedKey] = dedicatedValue } diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index 823d77bede..43376f2ed7 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -72,6 +72,9 @@ const RESTORED_KEYS = [ 'ATLAS_CLOUD_API_KEY', 'APISMART_API_KEY', 'APISMART_MODEL', + 'CONCENTRATE_API_KEY', + 'CONCENTRATE_BASE_URL', + 'CONCENTRATE_MODEL', 'CLINE_API_KEY', 'HICAP_API_KEY', 'CLOUDFLARE_API_TOKEN', @@ -278,6 +281,17 @@ function buildApismartProfile(overrides: Partial = {}): Provide }) } +function buildConcentrateProfile(overrides: Partial = {}): ProviderProfile { + return buildProfile({ + provider: 'concentrate', + name: 'Concentrate', + baseUrl: 'https://api.concentrate.ai/v1', + model: 'deepseek-v4-flash-0731', + apiKey: 'concentrate-test-key', + ...overrides, + }) +} + function buildClinePassProfile(overrides: Partial = {}): ProviderProfile { return buildProfile({ provider: 'clinepass', @@ -317,7 +331,7 @@ describe('applyProviderProfileToProcessEnv', () => { ) expect(process.env.OPENAI_AZURE_STYLE).toBe('1') - }) + }, 20_000) test('openai profile clears competing gemini/github flags', async () => { const { applyProviderProfileToProcessEnv } = @@ -952,6 +966,140 @@ describe('applyProviderProfileToProcessEnv', () => { expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('apismart') }) + test('concentrate profile applies OpenAI-compatible env with CONCENTRATE_API_KEY mirror', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + process.env.CLAUDE_CODE_USE_GEMINI = '1' + + applyProviderProfileToProcessEnv(buildConcentrateProfile()) + const { getAPIProvider: getFreshAPIProvider } = + await importFreshProvidersModule() + + expect(process.env.CLAUDE_CODE_USE_GEMINI).toBeUndefined() + expect(String(process.env.CLAUDE_CODE_USE_OPENAI)).toBe('1') + expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/v1') + expect(process.env.OPENAI_MODEL).toBe('deepseek-v4-flash-0731') + expect(process.env.OPENAI_API_KEY).toBe('concentrate-test-key') + expect(process.env.CONCENTRATE_API_KEY).toBe('concentrate-test-key') + expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('concentrate') + expect(getFreshAPIProvider()).toBe('openai') + }) + + test('concentrate profile clears a stale route-specific model before applying its saved model', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + + applyProviderProfileToProcessEnv(buildConcentrateProfile()) + + expect(process.env.CONCENTRATE_MODEL).toBeUndefined() + expect(process.env.OPENAI_MODEL).toBe('deepseek-v4-flash-0731') + }) + + test('concentrate profile without a base URL retains its dedicated credential for the default route', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + + applyProviderProfileToProcessEnv( + buildConcentrateProfile({ baseUrl: undefined }), + ) + + expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/v1') + expect(process.env.OPENAI_API_KEY).toBe('concentrate-test-key') + expect(process.env.CONCENTRATE_API_KEY).toBe('concentrate-test-key') + }) + + test('retargeted Concentrate profile withholds its dedicated credential', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + + applyProviderProfileToProcessEnv( + buildConcentrateProfile({ baseUrl: 'https://proxy.example/v1' }), + ) + + expect(process.env.OPENAI_BASE_URL).toBe('https://proxy.example/v1') + expect(process.env.OPENAI_API_KEY).toBeUndefined() + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('concentrate') + }) + + test('keyless Concentrate profile resolves CONCENTRATE_API_KEY without persisting it', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + process.env.CONCENTRATE_API_KEY = 'ambient-concentrate-key' + + applyProviderProfileToProcessEnv( + buildConcentrateProfile({ + apiKey: undefined, + baseUrl: 'https://api.concentrate.ai/v1', + }), + ) + + expect(process.env.OPENAI_API_KEY).toBe('ambient-concentrate-key') + expect(process.env.CONCENTRATE_API_KEY).toBe('ambient-concentrate-key') + expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('concentrate') + }, 20_000) + + test('keyless canonical Concentrate profile never promotes a generic OpenAI key', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + process.env.OPENAI_API_KEY = 'generic-openai-key' + + applyProviderProfileToProcessEnv( + buildConcentrateProfile({ + apiKey: undefined, + baseUrl: 'https://api.concentrate.ai/v1', + }), + ) + + expect(process.env.OPENAI_API_KEY).toBeUndefined() + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('concentrate') + }, 20_000) + + test('non-canonical Concentrate host path withholds the dedicated credential', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + + applyProviderProfileToProcessEnv( + buildConcentrateProfile({ + baseUrl: 'https://api.concentrate.ai/staging/v1', + }), + ) + + expect(process.env.OPENAI_BASE_URL).toBe( + 'https://api.concentrate.ai/staging/v1', + ) + expect(process.env.OPENAI_API_KEY).toBeUndefined() + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('concentrate') + }) + + test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])( + 'addProviderProfile drops placeholder Concentrate credential %s', + async placeholder => { + const { addProviderProfile, getProviderProfiles } = + await importFreshProviderProfileModules() + + saveMockGlobalConfig(current => ({ + ...current, + providerProfiles: [], + activeProviderProfileId: undefined, + })) + + const saved = addProviderProfile({ + provider: 'concentrate', + name: 'Concentrate', + baseUrl: 'https://api.concentrate.ai/v1', + model: 'deepseek-v4-flash-0731', + apiKey: placeholder, + }) + + expect(saved?.apiKey).toBeUndefined() + expect(getProviderProfiles()[0]?.apiKey).toBeUndefined() + }, + ) + test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])( 'addProviderProfile drops placeholder ApiSmart credential %s', async placeholder => { diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 4cc1aa0f04..7832b852b4 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -18,6 +18,7 @@ import { createProfileFile, saveProfileFile, buildBedrockProfileEnv, + buildConcentrateProfileEnv, buildGeminiProfileEnv, buildGithubProfileEnv, buildMiniMaxProfileEnv, @@ -55,6 +56,7 @@ import { isCloudflareBaseUrl, isClinePassBaseUrl, isCanonicalApismartInferenceBaseUrl, + isCanonicalConcentrateInferenceBaseUrl, isFireworksBaseUrl, isLongcatBaseUrl, isNearaiBaseUrl, @@ -161,6 +163,18 @@ function isApismartProfile(profile: ProviderProfile): boolean { return !baseUrl || isCanonicalApismartInferenceBaseUrl(baseUrl) } +function isConcentrateProfile(profile: ProviderProfile): boolean { + const { route } = resolveProfileCompatibility(profile.provider) + if (route.routeId !== 'concentrate') { + return false + } + const baseUrl = profile.baseUrl?.trim() + // Missing base URL resolves to the Concentrate default, which is canonical. + // Only the documented `/v1` inference URL may carry the dedicated key — + // host-only or path-suffixed Concentrate URLs are treated as retargeted. + return !baseUrl || isCanonicalConcentrateInferenceBaseUrl(baseUrl) +} + function deriveGithubEnterpriseUrl(baseUrl: string | undefined): string | undefined { if (!baseUrl?.trim()) return undefined try { @@ -972,7 +986,9 @@ export function applyProviderProfileToProcessEnv( ? normalizeXiaomiMimoBaseUrl(profile.baseUrl) ?? profile.baseUrl : route.routeId === 'apismart' && !profile.baseUrl?.trim() ? getRouteDefaultBaseUrl('apismart') ?? profile.baseUrl - : profile.baseUrl + : route.routeId === 'concentrate' && !profile.baseUrl?.trim() + ? getRouteDefaultBaseUrl('concentrate') ?? profile.baseUrl + : profile.baseUrl const openAIProfileEnv: ProfileEnv = { OPENAI_BASE_URL: normalizedProfileBaseUrl, OPENAI_MODEL: primaryModel, @@ -1001,7 +1017,13 @@ export function applyProviderProfileToProcessEnv( const withholdRetargetedApismartCredential = route.routeId === 'apismart' && !isApismartProfile(profile) - if (profile.apiKey && !withholdRetargetedApismartCredential) { + const withholdRetargetedConcentrateCredential = + route.routeId === 'concentrate' && !isConcentrateProfile(profile) + if ( + profile.apiKey && + !withholdRetargetedApismartCredential && + !withholdRetargetedConcentrateCredential + ) { openAIProfileEnv.OPENAI_API_KEY = profile.apiKey if (route.vendorId === 'minimax' || normalizedProfileBaseUrl.toLowerCase().includes('minimax')) { openAIProfileEnv.MINIMAX_API_KEY = profile.apiKey @@ -1038,6 +1060,9 @@ export function applyProviderProfileToProcessEnv( if (isApismartProfile(profile)) { openAIProfileEnv.APISMART_API_KEY = profile.apiKey } + if (isConcentrateProfile(profile)) { + openAIProfileEnv.CONCENTRATE_API_KEY = profile.apiKey + } if (isClinePassProfile(profile)) { openAIProfileEnv.CLINE_API_KEY = profile.apiKey } @@ -1094,10 +1119,29 @@ export function applyProviderProfileToProcessEnv( // must not receive the ambient key. if (isApismartProfile(profile)) { const ambientApismartKey = sanitizeApiKey(process.env.APISMART_API_KEY) - openAIProfileEnv.OPENAI_API_KEY = - openAIProfileEnv.OPENAI_API_KEY ?? ambientApismartKey - openAIProfileEnv.APISMART_API_KEY = - openAIProfileEnv.APISMART_API_KEY ?? ambientApismartKey + if (ambientApismartKey) { + openAIProfileEnv.OPENAI_API_KEY = + openAIProfileEnv.OPENAI_API_KEY ?? ambientApismartKey + openAIProfileEnv.APISMART_API_KEY = + openAIProfileEnv.APISMART_API_KEY ?? ambientApismartKey + } + } + } + // Concentrate is dedicatedCredentialsOnly: stamp the route identity and mirror + // the dedicated credential so the saved profile relaunches authenticated. + if (route.routeId === 'concentrate') { + openAIProfileEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'concentrate' + // Keyless canonical Concentrate profiles may resolve the ambient dedicated + // credential the same way ApiSmart does. + if (isConcentrateProfile(profile) && !profile.apiKey) { + const ambientConcentrateKey = sanitizeApiKey( + process.env.CONCENTRATE_API_KEY, + ) + if (ambientConcentrateKey) { + openAIProfileEnv.OPENAI_API_KEY = + openAIProfileEnv.OPENAI_API_KEY ?? ambientConcentrateKey + openAIProfileEnv.CONCENTRATE_API_KEY = ambientConcentrateKey + } } } if (route.gatewayId === 'nvidia-nim') { @@ -1387,6 +1431,7 @@ function buildOpenAICompatibleStartupEnv( const isAimlapiProfile = activeProfile.provider === 'aimlapi' || resolveRouteIdFromBaseUrl(activeProfile.baseUrl) === 'aimlapi' + const isConcentrateProfileFlag = isConcentrateProfile(activeProfile) if (activeProfile.apiKey && !withholdRetargetedApismartCredential) { const strictEnv = buildOpenAIProfileEnv({ @@ -1416,6 +1461,9 @@ function buildOpenAICompatibleStartupEnv( if (isApismartProfile(activeProfile)) { strictEnv.APISMART_API_KEY = activeProfile.apiKey } + if (isConcentrateProfileFlag) { + strictEnv.CONCENTRATE_API_KEY = activeProfile.apiKey + } if (isClinePassProfile(activeProfile)) { strictEnv.CLINE_API_KEY = activeProfile.apiKey } @@ -1468,6 +1516,9 @@ function buildOpenAICompatibleStartupEnv( if (resolveProfileRoute(activeProfile.provider).routeId === 'apismart') { env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'apismart' } + if (isConcentrateProfileFlag) { + env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'concentrate' + } if (activeProfile.apiKey && !withholdRetargetedApismartCredential) { env.OPENAI_API_KEY = activeProfile.apiKey if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) { @@ -1494,6 +1545,9 @@ function buildOpenAICompatibleStartupEnv( if (isApismartProfile(activeProfile)) { env.APISMART_API_KEY = activeProfile.apiKey } + if (isConcentrateProfileFlag) { + env.CONCENTRATE_API_KEY = activeProfile.apiKey + } if (isClinePassProfile(activeProfile)) { env.CLINE_API_KEY = activeProfile.apiKey } @@ -1696,6 +1750,19 @@ function buildStartupProfileFromActiveProfile( : null } + if (route.routeId === 'concentrate' && isConcentrateProfile(activeProfile)) { + const env = + buildConcentrateProfileEnv({ + model: getPrimaryModel(activeProfile.model), + baseUrl: activeProfile.baseUrl, + apiKey: activeProfile.apiKey, + processEnv: process.env, + }) ?? null + return env + ? { profile: 'openai', env: applySupportedProfileCustomHeaders(activeProfile, env) } + : null + } + if (route.vendorId === 'nearai') { const env = buildOpenAICompatibleStartupEnv(activeProfile) return env ? { profile: 'openai', env } : null From 1d5c66f0af46ed64a124029b2db06c08eb5d6b9f Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 13:11:28 -0700 Subject: [PATCH 05/19] fix(concentrate): protect credentials on noncanonical urls --- .env.example | 1 - src/utils/providerFlag.test.ts | 10 ++++++ src/utils/providerFlag.ts | 14 ++++++-- src/utils/providerProfile.test.ts | 11 +++++++ src/utils/providerProfile.ts | 37 ++++++++++++++++----- src/utils/providerProfiles.test.ts | 53 ++++++++++++++++++++++++++++++ src/utils/providerProfiles.ts | 24 +++++++++++--- 7 files changed, 132 insertions(+), 18 deletions(-) diff --git a/.env.example b/.env.example index 36b2b3be36..2e747ef21d 100644 --- a/.env.example +++ b/.env.example @@ -412,7 +412,6 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here # # Usage limits are displayed via /usage command. - # ----------------------------------------------------------------------------- # Option 15: Concentrate AI # ----------------------------------------------------------------------------- diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index 8c874ac793..7ed1feb1f8 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -1340,6 +1340,16 @@ describe('applyProviderFlag - concentrate', () => { expect(process.env.OPENAI_MODEL).toBe('claude-sonnet-5') }) + test('does not mirror the dedicated key to a noncanonical Concentrate URL', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' + process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/staging/v1' + + applyProviderFlag('concentrate', []) + + expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/staging/v1') + expect(process.env.OPENAI_API_KEY).toBeUndefined() + }) + test('explicit --model overrides CONCENTRATE_MODEL and OPENAI_MODEL', () => { process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' diff --git a/src/utils/providerFlag.ts b/src/utils/providerFlag.ts index 22ecb8bc44..a35d39986d 100644 --- a/src/utils/providerFlag.ts +++ b/src/utils/providerFlag.ts @@ -29,7 +29,10 @@ import { resolveRouteIdFromBaseUrl, } from '../integrations/index.js' import { PRESET_VENDOR_MAP } from '../integrations/compatibility.js' -import { isCanonicalApismartInferenceBaseUrl } from '../integrations/routeMetadata.js' +import { + isCanonicalApismartInferenceBaseUrl, + isCanonicalConcentrateInferenceBaseUrl, +} from '../integrations/routeMetadata.js' import { hasUsableOpenAICredential } from '../services/api/credentialPool.js' import { isFirstPartyAnthropicBaseUrlForEnv } from './anthropicBaseUrl.js' @@ -711,8 +714,13 @@ export function applyProviderFlag( // DedicatedCredentialsOnly: only CONCENTRATE_API_KEY authenticates this // route. Mirror it into OPENAI_API_KEY for the shared shim transport and // clear any stale generic key so ambient OpenAI credentials are never - // forwarded to Concentrate. - if (hasUsableOpenAICredential(process.env.CONCENTRATE_API_KEY)) { + // forwarded to Concentrate. A Concentrate key is only valid for its + // documented inference endpoint; do not mirror it to a same-host proxy + // or alternate path supplied through CONCENTRATE_BASE_URL. + if ( + hasUsableOpenAICredential(process.env.CONCENTRATE_API_KEY) && + isCanonicalConcentrateInferenceBaseUrl(getConfiguredOpenAIBaseUrl()) + ) { process.env.OPENAI_API_KEY = process.env.CONCENTRATE_API_KEY } else { delete process.env.OPENAI_API_KEY diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index 785c165441..4445823553 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -3169,3 +3169,14 @@ test('buildConcentrateProfileEnv reads CONCENTRATE_BASE_URL override', () => { assert.ok(env) assert.equal(env?.OPENAI_BASE_URL, 'https://api.concentrate.ai/v1') }) + +test('buildConcentrateProfileEnv refuses to serialize its credential for a noncanonical URL', () => { + const env = buildConcentrateProfileEnv({ + apiKey: 'concentrate-secret-key', + processEnv: { + CONCENTRATE_BASE_URL: 'https://api.concentrate.ai/staging/v1', + }, + }) + + assert.equal(env, null) +}) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 2df498463e..12f705ef6e 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -26,7 +26,7 @@ import { getRouteDefaultBaseUrl, getRouteDefaultModel, isCanonicalApismartInferenceBaseUrl, - isConcentrateBaseUrl, + isCanonicalConcentrateInferenceBaseUrl, isLongcatBaseUrl, normalizeXiaomiMimoBaseUrl, resolveRouteCredentialValue, @@ -721,6 +721,16 @@ export function buildConcentrateProfileEnv(options: { sanitizeProviderConfigValue(options.baseUrl, secretSource) || sanitizeProviderConfigValue(processEnv.CONCENTRATE_BASE_URL, secretSource) || sanitizeProviderConfigValue(processEnv.OPENAI_BASE_URL, secretSource) + // The dedicated credential belongs only to Concentrate's documented + // inference endpoint. Returning null lets the caller use its generic + // profile path without serializing this secret for a proxy or alternate + // same-host path. + if ( + configuredBaseUrl && + !isCanonicalConcentrateInferenceBaseUrl(configuredBaseUrl) + ) { + return null + } return { OPENAI_BASE_URL: configuredBaseUrl || defaultBaseUrl, @@ -2139,7 +2149,7 @@ export async function buildLaunchEnv(options: { const isNoncanonicalConcentrateLaunch = effectiveOpenAIRouteId === 'concentrate' && !!env.OPENAI_BASE_URL?.trim() && - !isConcentrateBaseUrl(env.OPENAI_BASE_URL) + !isCanonicalConcentrateInferenceBaseUrl(env.OPENAI_BASE_URL) const isNoncanonicalDedicatedOpenAILaunch = isNoncanonicalAimlapiLaunch || isNoncanonicalApismartLaunch || isNoncanonicalConcentrateLaunch if (isNoncanonicalDedicatedOpenAILaunch) { @@ -2223,7 +2233,7 @@ export async function buildLaunchEnv(options: { const withholdAmbientConcentrateKey = dedicatedKey === 'CONCENTRATE_API_KEY' && !!dedicatedBaseUrl && - !isConcentrateBaseUrl(dedicatedBaseUrl) + !isCanonicalConcentrateInferenceBaseUrl(dedicatedBaseUrl) const withholdAmbientDedicatedKey = withholdAmbientAimlapiKey || withholdAmbientApismartKey || withholdAmbientConcentrateKey // AIMLAPI accepts generic OpenAI credentials, but ApiSmart is @@ -2251,7 +2261,7 @@ export async function buildLaunchEnv(options: { dedicatedKey === 'CONCENTRATE_API_KEY' && effectiveOpenAIRouteId === 'concentrate' && !!dedicatedBaseUrl && - isConcentrateBaseUrl(dedicatedBaseUrl) && + isCanonicalConcentrateInferenceBaseUrl(dedicatedBaseUrl) && persistedOpenAICredential?.kind === 'usable' ? sanitizeApiKey(persistedOpenAICredential.value) : undefined @@ -2353,16 +2363,25 @@ export async function buildStartupEnvFromProfile(options?: { // If startup already has a concrete provider selection, keep trusting it. // This prevents legacy profiles or the fresh-install default from becoming // a silent third precedence layer over explicit env/flags. - // A retained ApiSmart proxy profile carries route identity specifically to - // withhold ambient dedicated credentials from its noncanonical endpoint. - // Do not let an env-only key skip that guard; the persisted profile must be - // applied first so buildLaunchEnv can preserve the proxy boundary. + // Retained dedicated-provider proxy profiles carry route identity specifically + // to withhold ambient credentials from their noncanonical endpoint. Do not + // let an env-only key skip that guard; the persisted profile must be applied + // first so buildLaunchEnv can preserve the credential boundary. const persistedApismartProxy = persisted?.profile === 'openai' && persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'apismart' && !!persisted.env.OPENAI_BASE_URL?.trim() && !isCanonicalApismartInferenceBaseUrl(persisted.env.OPENAI_BASE_URL) - if (hasConcreteProviderSelection(processEnv) && !persistedApismartProxy) { + const persistedConcentrateProxy = + persisted?.profile === 'openai' && + persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'concentrate' && + !!persisted.env.OPENAI_BASE_URL?.trim() && + !isCanonicalConcentrateInferenceBaseUrl(persisted.env.OPENAI_BASE_URL) + if ( + hasConcreteProviderSelection(processEnv) && + !persistedApismartProxy && + !persistedConcentrateProxy + ) { return processEnv } diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index 43376f2ed7..324995e087 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -3502,6 +3502,59 @@ describe('setActiveProviderProfile', () => { } }) + test('retargeted Concentrate profiles keep route identity but persist without their dedicated credential', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-')) + const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-')) + process.chdir(tempDir) + process.env.CLAUDE_CONFIG_DIR = configDir + + try { + const { setActiveProviderProfile } = + await importFreshProviderProfileModules() + const concentrateProfile = buildConcentrateProfile({ + id: 'concentrate_proxy', + baseUrl: 'https://api.concentrate.ai/staging/v1', + }) + + saveMockGlobalConfig(current => ({ + ...current, + providerProfiles: [concentrateProfile], + })) + + const result = setActiveProviderProfile('concentrate_proxy', { configDir }) + const persisted = JSON.parse( + readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'), + ) + + expect(result?.id).toBe('concentrate_proxy') + expect(persisted.profile).toBe('openai') + expect(persisted.env).toEqual({ + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'concentrate', + OPENAI_BASE_URL: 'https://api.concentrate.ai/staging/v1', + OPENAI_MODEL: 'deepseek-v4-flash-0731', + }) + + const { buildStartupEnvFromProfile } = await import( + `./providerProfile.js?ts=${Date.now()}-${Math.random()}` + ) + const startupEnv = await buildStartupEnvFromProfile({ + persisted, + processEnv: { + CONCENTRATE_API_KEY: 'ambient-concentrate-key', + OPENAI_API_KEY: 'ambient-concentrate-key', + }, + }) + + expect(startupEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('concentrate') + expect(startupEnv.CONCENTRATE_API_KEY).toBeUndefined() + expect(startupEnv.OPENAI_API_KEY).toBeUndefined() + } finally { + process.chdir(originalCwd) + rmSync(tempDir, { recursive: true, force: true }) + rmSync(configDir, { recursive: true, force: true }) + } + }) + test('persists Xiaomi MiMo profiles using a legacy-compatible openai startup profile', async () => { const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-')) const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-')) diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 7832b852b4..b675d92289 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -1425,15 +1425,23 @@ function buildOpenAICompatibleStartupEnv( if (isCodexBaseUrl(activeProfile.baseUrl)) { return null } + const activeProfileRouteId = resolveProfileRoute(activeProfile.provider).routeId const withholdRetargetedApismartCredential = - resolveProfileRoute(activeProfile.provider).routeId === 'apismart' && + activeProfileRouteId === 'apismart' && !isApismartProfile(activeProfile) + const withholdRetargetedConcentrateCredential = + activeProfileRouteId === 'concentrate' && + !isConcentrateProfile(activeProfile) const isAimlapiProfile = activeProfile.provider === 'aimlapi' || resolveRouteIdFromBaseUrl(activeProfile.baseUrl) === 'aimlapi' const isConcentrateProfileFlag = isConcentrateProfile(activeProfile) - if (activeProfile.apiKey && !withholdRetargetedApismartCredential) { + if ( + activeProfile.apiKey && + !withholdRetargetedApismartCredential && + !withholdRetargetedConcentrateCredential + ) { const strictEnv = buildOpenAIProfileEnv({ goal: 'balanced', model: activeProfile.model, @@ -1513,13 +1521,19 @@ function buildOpenAICompatibleStartupEnv( // Preserve ApiSmart identity on retargeted/proxy startup envs so relaunch // withholding can refuse ambient dedicated credentials. Canonical profiles // already stamp this via buildApismartProfileEnv. - if (resolveProfileRoute(activeProfile.provider).routeId === 'apismart') { + if (activeProfileRouteId === 'apismart') { env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'apismart' } - if (isConcentrateProfileFlag) { + // Preserve Concentrate identity for retargeted profiles too, so a later + // launch can apply the same dedicated-credential boundary. + if (activeProfileRouteId === 'concentrate') { env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'concentrate' } - if (activeProfile.apiKey && !withholdRetargetedApismartCredential) { + if ( + activeProfile.apiKey && + !withholdRetargetedApismartCredential && + !withholdRetargetedConcentrateCredential + ) { env.OPENAI_API_KEY = activeProfile.apiKey if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) { env.BNKR_API_KEY = activeProfile.apiKey From 90a1fb89cea05020b2ab03233f998188eb1aba89 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 13:13:28 -0700 Subject: [PATCH 06/19] fix(concentrate): drop legacy keys on retargeted profiles --- src/utils/providerProfile.test.ts | 18 ++++++++++++++++++ src/utils/providerProfile.ts | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index 4445823553..86053acf4e 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -3122,6 +3122,24 @@ test('openai launch withholds ambient Concentrate credentials from a keyless pro assert.equal(canonical.CONCENTRATE_API_KEY, 'ambient-concentrate-key') }) +test('openai launch removes a legacy persisted Concentrate key from a noncanonical URL', async () => { + const env = await buildLaunchEnv({ + profile: 'openai', + persisted: profile('openai', { + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'concentrate', + OPENAI_BASE_URL: 'https://api.concentrate.ai/staging/v1', + OPENAI_MODEL: 'deepseek-v4-flash-0731', + CONCENTRATE_API_KEY: 'legacy-concentrate-key', + }), + goal: 'coding', + processEnv: {}, + }) + + assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'concentrate') + assert.equal(env.OPENAI_API_KEY, undefined) + assert.equal(env.CONCENTRATE_API_KEY, undefined) +}) + test('buildStartupEnvFromProfile preserves Concentrate env-only setup over a saved profile', async () => { const env = await buildStartupEnvFromProfile({ persisted: profile('openai', { diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 12f705ef6e..283742c056 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -2236,6 +2236,14 @@ export async function buildLaunchEnv(options: { !isCanonicalConcentrateInferenceBaseUrl(dedicatedBaseUrl) const withholdAmbientDedicatedKey = withholdAmbientAimlapiKey || withholdAmbientApismartKey || withholdAmbientConcentrateKey + // Unlike the generic proxy-compatible routes above, Concentrate's + // dedicated key is never valid outside its canonical inference endpoint. + // Do not preserve a legacy persisted key for a retargeted Concentrate + // profile: older versions could have serialized one before this boundary + // was enforced. + if (withholdAmbientConcentrateKey) { + continue + } // AIMLAPI accepts generic OpenAI credentials, but ApiSmart is // dedicatedCredentialsOnly. Never promote a shell OPENAI_API_KEY into the // dedicated credential on relaunch. From fae0c210b97c261d56003f68a236d572ca3b0803 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 13:15:47 -0700 Subject: [PATCH 07/19] fix(concentrate): remove legacy generic keys on proxies --- src/utils/providerProfile.test.ts | 23 +++++++++++++++++++++++ src/utils/providerProfile.ts | 13 ++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index 86053acf4e..0b03202957 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -3140,6 +3140,29 @@ test('openai launch removes a legacy persisted Concentrate key from a noncanonic assert.equal(env.CONCENTRATE_API_KEY, undefined) }) +test('openai launch removes legacy generic credentials from a noncanonical Concentrate URL', async () => { + for (const [credentialEnvVar, credential] of [ + ['OPENAI_API_KEY', 'legacy-concentrate-key'], + ['OPENAI_API_KEYS', 'legacy-concentrate-key-1,legacy-concentrate-key-2'], + ] as const) { + const env = await buildLaunchEnv({ + profile: 'openai', + persisted: profile('openai', { + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'concentrate', + OPENAI_BASE_URL: 'https://api.concentrate.ai/staging/v1', + OPENAI_MODEL: 'deepseek-v4-flash-0731', + [credentialEnvVar]: credential, + }), + goal: 'coding', + processEnv: {}, + }) + + assert.equal(env.OPENAI_API_KEY, undefined) + assert.equal(env.OPENAI_API_KEYS, undefined) + assert.equal(env.CONCENTRATE_API_KEY, undefined) + } +}) + test('buildStartupEnvFromProfile preserves Concentrate env-only setup over a saved profile', async () => { const env = await buildStartupEnvFromProfile({ persisted: profile('openai', { diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 283742c056..e4fcf09a20 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -2155,9 +2155,16 @@ export async function buildLaunchEnv(options: { if (isNoncanonicalDedicatedOpenAILaunch) { delete env.OPENAI_API_KEY delete env.OPENAI_API_KEYS - const persistedCredential = resolveOpenAICredentialEnvSelection(persistedEnv) - if (persistedCredential) { - env[persistedCredential.envVar] = persistedCredential.value + // AIMLAPI and ApiSmart may retain a user-configured proxy credential in + // their persisted generic OpenAI field. Concentrate is different: its + // dedicated credential is never valid off the canonical endpoint, and + // older profiles could have stored that same secret under either generic + // alias. Do not resurrect it for a noncanonical Concentrate launch. + if (!isNoncanonicalConcentrateLaunch) { + const persistedCredential = resolveOpenAICredentialEnvSelection(persistedEnv) + if (persistedCredential) { + env[persistedCredential.envVar] = persistedCredential.value + } } // Custom authentication is a second credential channel, not just transport // metadata: the OpenAI shim sends OPENAI_AUTH_HEADER_VALUE as the request From 6992d4bc6c024c598e88668f2a5a634a19efe3df Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 13:19:34 -0700 Subject: [PATCH 08/19] fix(concentrate): reject noncanonical credential routes --- src/utils/providerProfile.test.ts | 13 +++++++++++++ src/utils/providerProfile.ts | 2 +- src/utils/providerValidation.test.ts | 12 ++++++++++++ src/utils/providerValidation.ts | 13 +++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index 0b03202957..f1eb016294 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -3221,3 +3221,16 @@ test('buildConcentrateProfileEnv refuses to serialize its credential for a nonca assert.equal(env, null) }) + +test('buildOpenAIProfileEnv does not stamp a generic key as Concentrate credentials on a noncanonical URL', () => { + const env = buildOpenAIProfileEnv({ + goal: 'coding', + baseUrl: 'https://api.concentrate.ai/staging/v1', + apiKey: 'generic-proxy-key', + processEnv: {}, + }) + + assert.ok(env) + assert.equal(env?.OPENAI_API_KEY, 'generic-proxy-key') + assert.equal(env?.CONCENTRATE_API_KEY, undefined) +}) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index e4fcf09a20..41e7e18882 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -994,7 +994,7 @@ export function buildOpenAIProfileEnv(options: { ...(resolveRouteIdFromBaseUrl(resolvedBaseUrl) === 'aimlapi' && key ? { AIMLAPI_API_KEY: key } : {}), - ...(resolveRouteIdFromBaseUrl(resolvedBaseUrl) === 'concentrate' && key + ...(isCanonicalConcentrateInferenceBaseUrl(resolvedBaseUrl) && key ? { CONCENTRATE_API_KEY: key } : {}), OPENAI_BASE_URL: resolvedBaseUrl, diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index e603ffe36f..5c310f5c67 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -211,6 +211,18 @@ test('noncanonical ApiSmart paths do not validate a dedicated credential', async ) }) +test('noncanonical Concentrate paths are rejected before runtime withholds the dedicated key', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.concentrate.ai/staging/v1' + process.env.CONCENTRATE_API_KEY = 'concentrate-key' + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_API_KEYS + + await expect(getProviderValidationError(process.env)).resolves.toBe( + 'Concentrate credentials require the canonical https://api.concentrate.ai/v1 endpoint.', + ) +}) + test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])( 'ApiSmart validation rejects placeholder APISMART_API_KEY %s', async placeholder => { diff --git a/src/utils/providerValidation.ts b/src/utils/providerValidation.ts index 69c6697f86..b2768cb26a 100644 --- a/src/utils/providerValidation.ts +++ b/src/utils/providerValidation.ts @@ -16,6 +16,7 @@ import { getRouteDescriptor, getRouteDefaultModel, isCanonicalApismartInferenceBaseUrl, + isCanonicalConcentrateInferenceBaseUrl, isCloudflareBaseUrl, isLongcatBaseUrl, matchHostnameAgainstRouteHosts, @@ -389,6 +390,18 @@ async function getDescriptorValidationError( return null } + // Concentrate's dedicated credential is valid only at its documented /v1 + // inference endpoint. Runtime correctly withholds it elsewhere; reject the + // configuration here as well so a noncanonical override cannot pass startup + // validation and then fail unauthenticated. + if ( + target.descriptor.id === 'concentrate' && + options.request?.baseUrl && + !isCanonicalConcentrateInferenceBaseUrl(options.request.baseUrl) + ) { + return 'Concentrate credentials require the canonical https://api.concentrate.ai/v1 endpoint.' + } + switch (validation.kind) { case 'credential-env': return getCredentialEnvValidationError(validation, env, options.request) From 1ee6d09b04f9dd0abdee851f642d14af4a6731fb Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 13:24:28 -0700 Subject: [PATCH 09/19] fix(concentrate): validate env-only credentials --- src/integrations/routeMetadata.ts | 8 ++++--- src/utils/providerValidation.test.ts | 34 +++++++++++++++++++++++++++- src/utils/providerValidation.ts | 27 +++++++++++++++------- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index 77e787217a..7a73202174 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -471,12 +471,12 @@ export function getConcentrateBaseUrlOverride( processEnv: NodeJS.ProcessEnv = process.env, ): string | undefined { const openAIBaseUrl = processEnv.OPENAI_BASE_URL?.trim() - if (isConcentrateBaseUrl(openAIBaseUrl)) { + if (isCanonicalConcentrateInferenceBaseUrl(openAIBaseUrl)) { return openAIBaseUrl } const openAIApiBase = processEnv.OPENAI_API_BASE?.trim() - if (isConcentrateBaseUrl(openAIApiBase)) { + if (isCanonicalConcentrateInferenceBaseUrl(openAIApiBase)) { return openAIApiBase } @@ -1240,7 +1240,9 @@ export function resolveRouteIdFromBaseUrl( if ( (route.id === 'cloudflare' && !isCloudflareBaseUrl(baseUrl)) || (route.id === 'longcat' && !isLongcatBaseUrl(baseUrl)) || - (route.id === 'apismart' && !isApismartBaseUrl(baseUrl)) + (route.id === 'apismart' && !isApismartBaseUrl(baseUrl)) || + (route.id === 'concentrate' && + !isCanonicalConcentrateInferenceBaseUrl(baseUrl)) ) { continue } diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index 5c310f5c67..cdf4bb90c9 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -33,6 +33,8 @@ const ENV_KEYS = [ 'MINIMAX_API_KEY', 'LONGCAT_API_KEY', 'APISMART_API_KEY', + 'CONCENTRATE_API_KEY', + 'CONCENTRATE_BASE_URL', 'NVIDIA_API_KEY', 'NVIDIA_NIM', 'BNKR_API_KEY', @@ -211,9 +213,19 @@ test('noncanonical ApiSmart paths do not validate a dedicated credential', async ) }) -test('noncanonical Concentrate paths are rejected before runtime withholds the dedicated key', async () => { +test('noncanonical Concentrate host paths fall back to generic OpenAI validation', async () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.OPENAI_BASE_URL = 'https://api.concentrate.ai/staging/v1' + process.env.OPENAI_API_KEY = 'generic-proxy-key' + delete process.env.CONCENTRATE_API_KEY + delete process.env.OPENAI_API_KEYS + + await expect(getProviderValidationError(process.env)).resolves.toBeNull() +}) + +test('noncanonical CONCENTRATE_BASE_URL is rejected instead of silently withholding its key', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/staging/v1' process.env.CONCENTRATE_API_KEY = 'concentrate-key' delete process.env.OPENAI_API_KEY delete process.env.OPENAI_API_KEYS @@ -223,6 +235,26 @@ test('noncanonical Concentrate paths are rejected before runtime withholds the d ) }) +test('Concentrate key-only setup validates before client defaults are applied', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.CONCENTRATE_API_KEY = 'concentrate-key' + + await expect(getProviderValidationError(process.env)).resolves.toBeNull() +}) + +test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])( + 'Concentrate validation rejects placeholder CONCENTRATE_API_KEY %s', + async placeholder => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.concentrate.ai/v1' + process.env.CONCENTRATE_API_KEY = placeholder + + await expect(getProviderValidationError(process.env)).resolves.toBe( + 'Concentrate auth is required. Set CONCENTRATE_API_KEY.', + ) + }, +) + test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])( 'ApiSmart validation rejects placeholder APISMART_API_KEY %s', async placeholder => { diff --git a/src/utils/providerValidation.ts b/src/utils/providerValidation.ts index b2768cb26a..f321db3d4d 100644 --- a/src/utils/providerValidation.ts +++ b/src/utils/providerValidation.ts @@ -140,7 +140,8 @@ function hasUsableCredentialEnvValue( envVar === 'OPENAI_API_KEYS' || envVar === 'OPENAI_API_KEY' || envVar === 'AIMLAPI_API_KEY' || - envVar === 'APISMART_API_KEY' + envVar === 'APISMART_API_KEY' || + envVar === 'CONCENTRATE_API_KEY' ) { return hasUsableOpenAICredential(value) } @@ -266,6 +267,14 @@ function getRuntimeValidationTarget( return undefined } + // The documented CONCENTRATE_API_KEY-only setup is routed before the client + // applies its default base URL. Select its descriptor directly so startup + // validates the dedicated credential instead of falling through to generic + // OpenAI validation. + if (resolveActiveRouteIdFromEnv(env) === 'concentrate') { + return validationTargets.find(target => target.descriptor.id === 'concentrate') + } + const request = resolveProviderRequest({ model: env.OPENAI_MODEL, baseUrl: env.OPENAI_BASE_URL, @@ -287,7 +296,9 @@ function getRuntimeValidationTarget( (target.descriptor.id === 'longcat' && !isLongcatBaseUrl(request.baseUrl)) || (target.descriptor.id === 'apismart' && - !isCanonicalApismartInferenceBaseUrl(request.baseUrl))) + !isCanonicalApismartInferenceBaseUrl(request.baseUrl)) || + (target.descriptor.id === 'concentrate' && + !isCanonicalConcentrateInferenceBaseUrl(request.baseUrl))) ) { return false } @@ -390,14 +401,14 @@ async function getDescriptorValidationError( return null } - // Concentrate's dedicated credential is valid only at its documented /v1 - // inference endpoint. Runtime correctly withholds it elsewhere; reject the - // configuration here as well so a noncanonical override cannot pass startup - // validation and then fail unauthenticated. + // An explicit Concentrate override carries the dedicated credential contract. + // Same-host proxy endpoints instead use the generic OpenAI configuration and + // credentials, so they are not selected as the Concentrate route above. + const concentrateBaseUrl = env.CONCENTRATE_BASE_URL?.trim() if ( target.descriptor.id === 'concentrate' && - options.request?.baseUrl && - !isCanonicalConcentrateInferenceBaseUrl(options.request.baseUrl) + concentrateBaseUrl && + !isCanonicalConcentrateInferenceBaseUrl(concentrateBaseUrl) ) { return 'Concentrate credentials require the canonical https://api.concentrate.ai/v1 endpoint.' } From 1eb5aba6f5101abe3fd99221f7eb40bc7455f938 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 13:27:43 -0700 Subject: [PATCH 10/19] fix(concentrate): align env-only route validation --- src/integrations/routeMetadata.test.ts | 9 +++++++++ src/integrations/routeMetadata.ts | 5 ++--- src/utils/providerValidation.test.ts | 12 ++++++++++++ src/utils/providerValidation.ts | 11 +++++++---- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/integrations/routeMetadata.test.ts b/src/integrations/routeMetadata.test.ts index 16884f74bf..ac4cd02063 100644 --- a/src/integrations/routeMetadata.test.ts +++ b/src/integrations/routeMetadata.test.ts @@ -1083,6 +1083,15 @@ test('resolveActiveRouteIdFromEnv does not infer Concentrate with a conflicting ).toBe('anthropic') }) +test('resolveActiveRouteIdFromEnv does not infer Concentrate from a same-host noncanonical base URL', () => { + expect( + resolveActiveRouteIdFromEnv({ + CLAUDE_CODE_USE_OPENAI: '1', + OPENAI_BASE_URL: 'https://api.concentrate.ai/staging/v1', + }), + ).toBe('custom') +}) + test('resolveActiveRouteIdFromEnv keeps an explicit non-OpenAI provider over Concentrate key-only setup', () => { expect( resolveActiveRouteIdFromEnv({ diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index 7a73202174..883ae50f9d 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -471,12 +471,12 @@ export function getConcentrateBaseUrlOverride( processEnv: NodeJS.ProcessEnv = process.env, ): string | undefined { const openAIBaseUrl = processEnv.OPENAI_BASE_URL?.trim() - if (isCanonicalConcentrateInferenceBaseUrl(openAIBaseUrl)) { + if (isConcentrateBaseUrl(openAIBaseUrl)) { return openAIBaseUrl } const openAIApiBase = processEnv.OPENAI_API_BASE?.trim() - if (isCanonicalConcentrateInferenceBaseUrl(openAIApiBase)) { + if (isConcentrateBaseUrl(openAIApiBase)) { return openAIApiBase } @@ -960,7 +960,6 @@ export function hasConcentrateEnvOnlyProviderIntent( // only key forwarded to api.concentrate.ai. return ( (hasUsableOpenAICredential(processEnv.CONCENTRATE_API_KEY) || - getConcentrateBaseUrlOverride(processEnv) !== undefined || hasNonEmptyEnvValue(processEnv.CONCENTRATE_BASE_URL) || hasNonEmptyEnvValue(processEnv.CONCENTRATE_MODEL)) && !hasConflictingOpenAIBaseUrlForRoute(processEnv, isConcentrateBaseUrl) && diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index cdf4bb90c9..0725c8d46f 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -223,6 +223,18 @@ test('noncanonical Concentrate host paths fall back to generic OpenAI validation await expect(getProviderValidationError(process.env)).resolves.toBeNull() }) +test('noncanonical same-host base rejects a selected CONCENTRATE_API_KEY route', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.concentrate.ai/staging/v1' + process.env.CONCENTRATE_API_KEY = 'concentrate-key' + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_API_KEYS + + await expect(getProviderValidationError(process.env)).resolves.toBe( + 'Concentrate credentials require the canonical https://api.concentrate.ai/v1 endpoint.', + ) +}) + test('noncanonical CONCENTRATE_BASE_URL is rejected instead of silently withholding its key', async () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/staging/v1' diff --git a/src/utils/providerValidation.ts b/src/utils/providerValidation.ts index f321db3d4d..a5ac167df1 100644 --- a/src/utils/providerValidation.ts +++ b/src/utils/providerValidation.ts @@ -401,10 +401,13 @@ async function getDescriptorValidationError( return null } - // An explicit Concentrate override carries the dedicated credential contract. - // Same-host proxy endpoints instead use the generic OpenAI configuration and - // credentials, so they are not selected as the Concentrate route above. - const concentrateBaseUrl = env.CONCENTRATE_BASE_URL?.trim() + // An explicit Concentrate credential carries the dedicated contract. A + // same-host proxy with only generic OpenAI variables is not selected as this + // route, but once Concentrate is selected its base must be canonical. + const concentrateBaseUrl = + env.CONCENTRATE_BASE_URL?.trim() || + env.OPENAI_BASE_URL?.trim() || + env.OPENAI_API_BASE?.trim() if ( target.descriptor.id === 'concentrate' && concentrateBaseUrl && From e88cbfacbddc37395dc31ca6be02133680842de3 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 13:30:59 -0700 Subject: [PATCH 11/19] fix(concentrate): clear headers before client setup --- src/services/api/client.test.ts | 2 + src/services/api/client.ts | 84 ++++++++++++++------------------- 2 files changed, 37 insertions(+), 49 deletions(-) diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts index 26b200bdb8..6b6576e36d 100644 --- a/src/services/api/client.test.ts +++ b/src/services/api/client.test.ts @@ -793,6 +793,7 @@ test('routes env-only Concentrate requests through the OpenAI-compatible shim', process.env.CONCENTRATE_API_KEY = 'concentrate-test-key' process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/v1' process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Proxy-Auth: ambient-proxy-secret' globalThis.fetch = (async (input, init) => { capturedUrl = @@ -846,6 +847,7 @@ test('routes env-only Concentrate requests through the OpenAI-compatible shim', expect(capturedUrl).toBe('https://api.concentrate.ai/v1/chat/completions') expect(capturedHeaders?.get('authorization')).toBe('Bearer concentrate-test-key') + expect(capturedHeaders?.get('x-proxy-auth')).toBeNull() expect(capturedBody?.model).toBe('claude-sonnet-5') expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/v1') expect(process.env.OPENAI_API_KEY).toBe('concentrate-test-key') diff --git a/src/services/api/client.ts b/src/services/api/client.ts index 800018db2c..1b48d7d071 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -552,6 +552,41 @@ export async function getAnthropicClient({ ? 'max' : standardEffortToOpenAI(appliedEffortLevel)) : undefined + // Normalize env-only routes before snapshotting custom headers. Dedicated + // routes such as Concentrate deliberately clear inherited proxy headers; + // doing that after getCustomHeaders() would leave a copied secret in the + // request defaults. + const envOnlyProviderRouteId = resolveEnvOnlyProviderRouteId(process.env) + const useMiniMaxEnvOnlyProvider = shouldUseMiniMaxEnvOnlyProvider( + model, + envOnlyProviderRouteId, + ) + const useXiaomiMimoEnvOnlyProvider = + envOnlyProviderRouteId === 'xiaomi-mimo' && !useMiniMaxEnvOnlyProvider + const useXaiEnvOnlyProvider = + envOnlyProviderRouteId === 'xai' && !useMiniMaxEnvOnlyProvider + const useNearaiEnvOnlyProvider = + envOnlyProviderRouteId === 'nearai' && !useMiniMaxEnvOnlyProvider + const useFireworksEnvOnlyProvider = + envOnlyProviderRouteId === 'fireworks' && !useMiniMaxEnvOnlyProvider + const useLongcatEnvOnlyProvider = + envOnlyProviderRouteId === 'longcat' && !useMiniMaxEnvOnlyProvider + const useAimlapiEnvOnlyProvider = + envOnlyProviderRouteId === 'aimlapi' && !useMiniMaxEnvOnlyProvider + const useApismartEnvOnlyProvider = + envOnlyProviderRouteId === 'apismart' && !useMiniMaxEnvOnlyProvider + const useConcentrateEnvOnlyProvider = + envOnlyProviderRouteId === 'concentrate' && !useMiniMaxEnvOnlyProvider + if (useMiniMaxEnvOnlyProvider) applyMiniMaxEnvOnlyDefaults(model) + if (useXiaomiMimoEnvOnlyProvider) applyXiaomiMimoEnvOnlyDefaults() + if (useXaiEnvOnlyProvider) applyXaiEnvOnlyDefaults() + if (useNearaiEnvOnlyProvider) applyNearaiEnvOnlyDefaults() + if (useFireworksEnvOnlyProvider) applyFireworksEnvOnlyDefaults() + if (useLongcatEnvOnlyProvider) applyLongcatEnvOnlyDefaults() + if (useAimlapiEnvOnlyProvider) applyAimlapiEnvOnlyDefaults() + if (useApismartEnvOnlyProvider) applyApismartEnvOnlyDefaults() + if (useConcentrateEnvOnlyProvider) applyConcentrateEnvOnlyDefaults() + const containerId = process.env.CLAUDE_CODE_CONTAINER_ID const remoteSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID const clientApp = process.env.CLAUDE_AGENT_SDK_CLIENT_APP @@ -582,55 +617,6 @@ export async function getAnthropicClient({ defaultHeaders['x-anthropic-additional-protection'] = 'true' } - const envOnlyProviderRouteId = resolveEnvOnlyProviderRouteId(process.env) - const useMiniMaxEnvOnlyProvider = shouldUseMiniMaxEnvOnlyProvider( - model, - envOnlyProviderRouteId, - ) - const useXiaomiMimoEnvOnlyProvider = - envOnlyProviderRouteId === 'xiaomi-mimo' && !useMiniMaxEnvOnlyProvider - const useXaiEnvOnlyProvider = - envOnlyProviderRouteId === 'xai' && !useMiniMaxEnvOnlyProvider - const useNearaiEnvOnlyProvider = - envOnlyProviderRouteId === 'nearai' && !useMiniMaxEnvOnlyProvider - const useFireworksEnvOnlyProvider = - envOnlyProviderRouteId === 'fireworks' && !useMiniMaxEnvOnlyProvider - const useLongcatEnvOnlyProvider = - envOnlyProviderRouteId === 'longcat' && !useMiniMaxEnvOnlyProvider - const useAimlapiEnvOnlyProvider = - envOnlyProviderRouteId === 'aimlapi' && !useMiniMaxEnvOnlyProvider - const useApismartEnvOnlyProvider = - envOnlyProviderRouteId === 'apismart' && !useMiniMaxEnvOnlyProvider - const useConcentrateEnvOnlyProvider = - envOnlyProviderRouteId === 'concentrate' && !useMiniMaxEnvOnlyProvider - if (useMiniMaxEnvOnlyProvider) { - applyMiniMaxEnvOnlyDefaults(model) - } - if (useXiaomiMimoEnvOnlyProvider) { - applyXiaomiMimoEnvOnlyDefaults() - } - if (useXaiEnvOnlyProvider) { - applyXaiEnvOnlyDefaults() - } - if (useNearaiEnvOnlyProvider) { - applyNearaiEnvOnlyDefaults() - } - if (useFireworksEnvOnlyProvider) { - applyFireworksEnvOnlyDefaults() - } - if (useLongcatEnvOnlyProvider) { - applyLongcatEnvOnlyDefaults() - } - if (useAimlapiEnvOnlyProvider) { - applyAimlapiEnvOnlyDefaults() - } - if (useApismartEnvOnlyProvider) { - applyApismartEnvOnlyDefaults() - } - if (useConcentrateEnvOnlyProvider) { - applyConcentrateEnvOnlyDefaults() - } - const apiProvider = getAPIProvider() const isFirstPartyBaseUrl = isFirstPartyAnthropicBaseUrl() const shouldUseFirstPartyAuth = shouldUseFirstPartyAnthropicAuthForProvider({ From d30d8a9221f3782802d1d9a1dfd1a7be21197d90 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 13:33:31 -0700 Subject: [PATCH 12/19] test(validation): isolate Concentrate model env --- src/utils/providerValidation.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index 0725c8d46f..9816371196 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -35,6 +35,7 @@ const ENV_KEYS = [ 'APISMART_API_KEY', 'CONCENTRATE_API_KEY', 'CONCENTRATE_BASE_URL', + 'CONCENTRATE_MODEL', 'NVIDIA_API_KEY', 'NVIDIA_NIM', 'BNKR_API_KEY', From a52cf2b4a98e4c45c845b8eeb8b260e4bf76c021 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 14:07:31 -0700 Subject: [PATCH 13/19] fix(concentrate): honor provider precedence and model defaults --- src/integrations/routeMetadata.test.ts | 4 ++++ src/integrations/routeMetadata.ts | 2 ++ src/utils/model/model.ts | 10 +++++++++- src/utils/providerFlag.test.ts | 20 ++++++++++++++++++++ src/utils/providerFlag.ts | 9 +++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/integrations/routeMetadata.test.ts b/src/integrations/routeMetadata.test.ts index ac4cd02063..abf722cf58 100644 --- a/src/integrations/routeMetadata.test.ts +++ b/src/integrations/routeMetadata.test.ts @@ -1101,6 +1101,10 @@ test('resolveActiveRouteIdFromEnv keeps an explicit non-OpenAI provider over Con ).toBe('gemini') }) +test('resolveActiveRouteIdFromEnv honors an explicit OpenAI opt-out over Concentrate', () => { + expect(resolveActiveRouteIdFromEnv({ CLAUDE_CODE_USE_OPENAI: '0', CONCENTRATE_API_KEY: 'concentrate-key' })).not.toBe('concentrate') +}) + test('resolveActiveRouteIdFromEnv refines generic OpenAI profile by Concentrate base URL', () => { expect( resolveActiveRouteIdFromEnv({ diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index 883ae50f9d..26457b387b 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -963,6 +963,8 @@ export function hasConcentrateEnvOnlyProviderIntent( hasNonEmptyEnvValue(processEnv.CONCENTRATE_BASE_URL) || hasNonEmptyEnvValue(processEnv.CONCENTRATE_MODEL)) && !hasConflictingOpenAIBaseUrlForRoute(processEnv, isConcentrateBaseUrl) && + !(processEnv.CLAUDE_CODE_USE_OPENAI !== undefined && + !isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) && hasNoExplicitNonOpenAIProvider(processEnv) ) } diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 71ea3c4e50..cac1e903c0 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -35,7 +35,7 @@ import { type ModelAlias, isModelAlias } from './aliases.js' import { capitalize } from '../stringUtils.js' import { DEFAULT_GEMINI_MODEL } from '../providerProfile.js' import { getAntModelOverrideConfig, resolveAntModel } from './antModels.js' -import { getRouteDefaultModel } from '../../integrations/routeMetadata.js' +import { getRouteDefaultModel, resolveActiveRouteIdFromEnv } from '../../integrations/routeMetadata.js' export type ModelShortName = string export type ModelName = string @@ -373,6 +373,14 @@ export function getRuntimeMainLoopModel(params: { * @returns The default model setting to use */ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { + if (resolveActiveRouteIdFromEnv(process.env) === 'concentrate') { + return ( + process.env.CONCENTRATE_MODEL || + process.env.OPENAI_MODEL || + getRouteDefaultModel('concentrate') || + 'deepseek-v4-flash-0731' + ) + } // Custom Anthropic-compatible endpoints intentionally retain the legacy // firstParty provider category, so prefer their explicitly configured model // before the subscription and PAYG defaults below. diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index 7ed1feb1f8..b06e55f90e 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -207,6 +207,16 @@ describe('applyProviderFlag - anthropic', () => { expect(process.env.APISMART_API_KEY).toBeUndefined() expect(process.env.APISMART_MODEL).toBeUndefined() }) + + test('does not leave Concentrate env-only selection active', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-key' + process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/v1' + process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + applyProviderFlag('anthropic', []) + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + expect(process.env.CONCENTRATE_BASE_URL).toBeUndefined() + expect(process.env.CONCENTRATE_MODEL).toBeUndefined() + }) }) describe('applyProviderFlag - custom Anthropic-compatible', () => { @@ -315,6 +325,16 @@ describe('applyProviderFlag - openai', () => { expect(process.env.APISMART_API_KEY).toBeUndefined() expect(process.env.APISMART_MODEL).toBeUndefined() }) + + test('does not leave Concentrate env-only selection active', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-key' + process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/v1' + process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + applyProviderFlag('openai', []) + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + expect(process.env.CONCENTRATE_BASE_URL).toBeUndefined() + expect(process.env.CONCENTRATE_MODEL).toBeUndefined() + }) }) describe('applyProviderFlag - cloudflare', () => { diff --git a/src/utils/providerFlag.ts b/src/utils/providerFlag.ts index a35d39986d..67eb3a8713 100644 --- a/src/utils/providerFlag.ts +++ b/src/utils/providerFlag.ts @@ -418,6 +418,9 @@ export function applyProviderFlag( // OpenAI-compatible env-only route override it later in startup. delete process.env.APISMART_API_KEY delete process.env.APISMART_MODEL + delete process.env.CONCENTRATE_API_KEY + delete process.env.CONCENTRATE_BASE_URL + delete process.env.CONCENTRATE_MODEL delete process.env.ANTHROPIC_AUTH_TOKEN delete process.env.ANTHROPIC_CUSTOM_HEADERS break @@ -454,6 +457,9 @@ export function applyProviderFlag( delete process.env.OPENAI_AUTH_HEADER delete process.env.OPENAI_AUTH_SCHEME delete process.env.OPENAI_AUTH_HEADER_VALUE + delete process.env.CONCENTRATE_API_KEY + delete process.env.CONCENTRATE_BASE_URL + delete process.env.CONCENTRATE_MODEL process.env.ANTHROPIC_MODEL ??= defaultModel if (model) process.env.ANTHROPIC_MODEL = model break @@ -464,6 +470,9 @@ export function applyProviderFlag( // dedicated env-only gateway during client startup. delete process.env.APISMART_API_KEY delete process.env.APISMART_MODEL + delete process.env.CONCENTRATE_API_KEY + delete process.env.CONCENTRATE_BASE_URL + delete process.env.CONCENTRATE_MODEL if (model) process.env.OPENAI_MODEL = model break From fd93e09cdec63b90619289e4964000c41ffe2eab Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 14:09:44 -0700 Subject: [PATCH 14/19] test(concentrate): cover model resolution precedence --- .../model/model.openai-shim-providers.test.ts | 30 +++++++++++++++++++ src/utils/model/model.ts | 6 ++++ src/utils/providerFlag.test.ts | 15 ++++++++++ 3 files changed, 51 insertions(+) diff --git a/src/utils/model/model.openai-shim-providers.test.ts b/src/utils/model/model.openai-shim-providers.test.ts index 17c50fe602..446b710bc8 100644 --- a/src/utils/model/model.openai-shim-providers.test.ts +++ b/src/utils/model/model.openai-shim-providers.test.ts @@ -75,6 +75,9 @@ const SAVED_ENV = { ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL, ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL, MIMO_API_KEY: process.env.MIMO_API_KEY, + CONCENTRATE_API_KEY: process.env.CONCENTRATE_API_KEY, + CONCENTRATE_BASE_URL: process.env.CONCENTRATE_BASE_URL, + CONCENTRATE_MODEL: process.env.CONCENTRATE_MODEL, OPENAI_MODEL: process.env.OPENAI_MODEL, OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, CODEX_API_KEY: process.env.CODEX_API_KEY, @@ -133,6 +136,9 @@ beforeEach(async () => { delete process.env.MINIMAX_API_KEY delete process.env.ANTHROPIC_MODEL delete process.env.MIMO_API_KEY + delete process.env.CONCENTRATE_API_KEY + delete process.env.CONCENTRATE_BASE_URL + delete process.env.CONCENTRATE_MODEL delete process.env.OPENAI_MODEL delete process.env.OPENAI_BASE_URL delete process.env.CODEX_API_KEY @@ -340,6 +346,30 @@ test('getDefaultMainLoopModelSetting defaults MiniMax to M3', async () => { expect(getDefaultMainLoopModel()).toBe('MiniMax-M3') }) +test('Concentrate selects its dedicated model before client normalization', async () => { + // getMainLoopModel runs before getAnthropicClient mirrors Concentrate into + // OPENAI_MODEL. A saved model must not win during that interval. + saveGlobalConfig(current => ({ ...current, model: 'stale-other-provider-model' })) + process.env.CONCENTRATE_API_KEY = 'concentrate-test' + process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + + const { + getDefaultMainLoopModelSetting, + getMainLoopModel, + getUserSpecifiedModelSetting, + } = await importFreshModelModule() + expect(getUserSpecifiedModelSetting()).toBe('claude-sonnet-5') + expect(getDefaultMainLoopModelSetting()).toBe('claude-sonnet-5') + expect(getMainLoopModel()).toBe('claude-sonnet-5') +}) + +test('Concentrate uses its descriptor default before client normalization', async () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-test' + + const { getDefaultMainLoopModelSetting } = await importFreshModelModule() + expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash-0731') +}) + test('getDefaultMainLoopModelSetting uses the NVIDIA NIM route model', async () => { process.env.NVIDIA_NIM = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1' diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index cac1e903c0..e2504af7e3 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -140,6 +140,7 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined { // settings.model, so switching from (say) Moonshot to Codex kept firing // `kimi-k2.6` at the Codex endpoint and getting 400s. const provider = getAPIProvider() + const activeRouteId = resolveActiveRouteIdFromEnv(process.env) const isOpenAIShimProvider = provider === 'openai' || provider === 'codex' || @@ -149,6 +150,11 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined { provider === 'xiaomi-mimo' || provider === 'xai' specifiedModel = + // Concentrate is an env-only OpenAI-compatible route. Model selection + // happens before the client applies its OpenAI-compatible defaults, so + // consume its dedicated setting here rather than falling through to a + // saved model from an unrelated provider. + (activeRouteId === 'concentrate' ? process.env.CONCENTRATE_MODEL : undefined) || (provider === 'gemini' ? process.env.GEMINI_MODEL : undefined) || (provider === 'mistral' ? process.env.MISTRAL_MODEL : undefined) || (provider === 'minimax' ? getMiniMaxModelEnv() : undefined) || diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index b06e55f90e..aca9e1f93c 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -277,6 +277,21 @@ describe('applyProviderFlag - custom Anthropic-compatible', () => { expect(process.env.ANTHROPIC_MODEL).toBe('proxy-model') }) + test('does not leave Concentrate env-only selection active', () => { + process.env.ANTHROPIC_BASE_URL = 'https://proxy.example/v1' + process.env.ANTHROPIC_AUTH_TOKEN = 'proxy-token' + process.env.CONCENTRATE_API_KEY = 'concentrate-key' + process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/v1' + process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + + const result = applyProviderFlag('custom-anthropic', []) + + expect(result.error).toBeUndefined() + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + expect(process.env.CONCENTRATE_BASE_URL).toBeUndefined() + expect(process.env.CONCENTRATE_MODEL).toBeUndefined() + }) + test('accepts native x-api-key authentication', () => { process.env.ANTHROPIC_BASE_URL = 'https://proxy.example/v1' process.env.ANTHROPIC_API_KEY = 'stale-first-party-key' From 922b2858820c4657c1fe60b38a6ee586a538905a Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 14:12:08 -0700 Subject: [PATCH 15/19] fix(concentrate): preserve legacy model fallback --- src/utils/model/model.openai-shim-providers.test.ts | 11 +++++++++++ src/utils/model/model.ts | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/utils/model/model.openai-shim-providers.test.ts b/src/utils/model/model.openai-shim-providers.test.ts index 446b710bc8..7d20705c80 100644 --- a/src/utils/model/model.openai-shim-providers.test.ts +++ b/src/utils/model/model.openai-shim-providers.test.ts @@ -363,6 +363,17 @@ test('Concentrate selects its dedicated model before client normalization', asyn expect(getMainLoopModel()).toBe('claude-sonnet-5') }) +test('Concentrate honors its legacy OpenAI model fallback before client normalization', async () => { + saveGlobalConfig(current => ({ ...current, model: 'stale-other-provider-model' })) + process.env.CONCENTRATE_API_KEY = 'concentrate-test' + process.env.OPENAI_MODEL = 'legacy-concentrate-model' + + const { getMainLoopModel, getUserSpecifiedModelSetting } = + await importFreshModelModule() + expect(getUserSpecifiedModelSetting()).toBe('legacy-concentrate-model') + expect(getMainLoopModel()).toBe('legacy-concentrate-model') +}) + test('Concentrate uses its descriptor default before client normalization', async () => { process.env.CONCENTRATE_API_KEY = 'concentrate-test' diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index e2504af7e3..07b54708af 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -154,7 +154,9 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined { // happens before the client applies its OpenAI-compatible defaults, so // consume its dedicated setting here rather than falling through to a // saved model from an unrelated provider. - (activeRouteId === 'concentrate' ? process.env.CONCENTRATE_MODEL : undefined) || + (activeRouteId === 'concentrate' + ? process.env.CONCENTRATE_MODEL || process.env.OPENAI_MODEL + : undefined) || (provider === 'gemini' ? process.env.GEMINI_MODEL : undefined) || (provider === 'mistral' ? process.env.MISTRAL_MODEL : undefined) || (provider === 'minimax' ? getMiniMaxModelEnv() : undefined) || From aa2bb4a148ec3c96f722f7606fb0810fc554f2c5 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 14:13:12 -0700 Subject: [PATCH 16/19] fix(concentrate): normalize early model selection --- .../model/model.openai-shim-providers.test.ts | 18 +++++++++++ src/utils/model/model.ts | 32 +++++++++++-------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/utils/model/model.openai-shim-providers.test.ts b/src/utils/model/model.openai-shim-providers.test.ts index 7d20705c80..ff9b0c98de 100644 --- a/src/utils/model/model.openai-shim-providers.test.ts +++ b/src/utils/model/model.openai-shim-providers.test.ts @@ -381,6 +381,24 @@ test('Concentrate uses its descriptor default before client normalization', asyn expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash-0731') }) +test.each(['null', 'undefined', ' '])( + 'Concentrate ignores unusable dedicated model value %p before client normalization', + async value => { + saveGlobalConfig(current => ({ ...current, model: 'stale-other-provider-model' })) + process.env.CONCENTRATE_API_KEY = 'concentrate-test' + process.env.CONCENTRATE_MODEL = value + + const { + getDefaultMainLoopModelSetting, + getMainLoopModel, + getUserSpecifiedModelSetting, + } = await importFreshModelModule() + expect(getUserSpecifiedModelSetting()).toBeUndefined() + expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash-0731') + expect(getMainLoopModel()).toBe('deepseek-v4-flash-0731') + }, +) + test('getDefaultMainLoopModelSetting uses the NVIDIA NIM route model', async () => { process.env.NVIDIA_NIM = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1' diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 07b54708af..12b32bf075 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -51,6 +51,13 @@ function normalizeModelSetting(value: unknown): ModelName | ModelAlias | undefin return trimmed.length > 0 ? trimmed : undefined } +function getUsableProviderConfigModel(value: string | undefined): string | undefined { + const normalized = normalizeModelSetting(value) + if (!normalized) return undefined + const lowercase = normalized.toLowerCase() + return lowercase === 'undefined' || lowercase === 'null' ? undefined : normalized +} + export function getSmallFastModel(): ModelName { if (process.env.ANTHROPIC_SMALL_FAST_MODEL) return process.env.ANTHROPIC_SMALL_FAST_MODEL if (isCustomAnthropicProvider()) { @@ -149,21 +156,20 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined { provider === 'minimax' || provider === 'xiaomi-mimo' || provider === 'xai' - specifiedModel = + specifiedModel = activeRouteId === 'concentrate' // Concentrate is an env-only OpenAI-compatible route. Model selection // happens before the client applies its OpenAI-compatible defaults, so // consume its dedicated setting here rather than falling through to a // saved model from an unrelated provider. - (activeRouteId === 'concentrate' - ? process.env.CONCENTRATE_MODEL || process.env.OPENAI_MODEL - : undefined) || - (provider === 'gemini' ? process.env.GEMINI_MODEL : undefined) || - (provider === 'mistral' ? process.env.MISTRAL_MODEL : undefined) || - (provider === 'minimax' ? getMiniMaxModelEnv() : undefined) || - (isOpenAIShimProvider ? process.env.OPENAI_MODEL : undefined) || - (provider === 'firstParty' ? process.env.ANTHROPIC_MODEL : undefined) || - setting || - undefined + ? getUsableProviderConfigModel(process.env.CONCENTRATE_MODEL) || + getUsableProviderConfigModel(process.env.OPENAI_MODEL) + : (provider === 'gemini' ? process.env.GEMINI_MODEL : undefined) || + (provider === 'mistral' ? process.env.MISTRAL_MODEL : undefined) || + (provider === 'minimax' ? getMiniMaxModelEnv() : undefined) || + (isOpenAIShimProvider ? process.env.OPENAI_MODEL : undefined) || + (provider === 'firstParty' ? process.env.ANTHROPIC_MODEL : undefined) || + setting || + undefined } // Ignore the user-specified model if it's not in the availableModels allowlist. @@ -383,8 +389,8 @@ export function getRuntimeMainLoopModel(params: { export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { if (resolveActiveRouteIdFromEnv(process.env) === 'concentrate') { return ( - process.env.CONCENTRATE_MODEL || - process.env.OPENAI_MODEL || + getUsableProviderConfigModel(process.env.CONCENTRATE_MODEL) || + getUsableProviderConfigModel(process.env.OPENAI_MODEL) || getRouteDefaultModel('concentrate') || 'deepseek-v4-flash-0731' ) From 3d6a601734fae57b64ff4344cdb31368a1a4f7c5 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 14:37:11 -0700 Subject: [PATCH 17/19] fix(concentrate): validate and select rejected models safely --- .../model/model.openai-shim-providers.test.ts | 37 ++++++++++++++++++- src/utils/model/model.ts | 17 +++++++-- src/utils/providerValidation.test.ts | 11 ++++++ src/utils/providerValidation.ts | 12 +++--- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/utils/model/model.openai-shim-providers.test.ts b/src/utils/model/model.openai-shim-providers.test.ts index ff9b0c98de..a0ea8ca488 100644 --- a/src/utils/model/model.openai-shim-providers.test.ts +++ b/src/utils/model/model.openai-shim-providers.test.ts @@ -14,6 +14,8 @@ import { clearPluginSettingsBase, resetSettingsCache, } from '../settings/settingsCache.js' +let allowedModels: Set | undefined + async function importFreshModelModule() { mock.restore() const getAPIProvider = () => { @@ -44,7 +46,7 @@ async function importFreshModelModule() { getAPIProvider() === 'firstParty' && !!process.env.ANTHROPIC_BASE_URL, })) mock.module('./modelAllowlist.js', () => ({ - isModelAllowed: () => true, + isModelAllowed: (model: string) => allowedModels?.has(model) ?? true, })) const nonce = `${Date.now()}-${Math.random()}` return import(`./model.js?ts=${nonce}`) @@ -125,6 +127,7 @@ beforeEach(async () => { resetStateForTests() resetSettingsCache() clearPluginSettingsBase() + allowedModels = undefined delete process.env.CLAUDE_CODE_USE_OPENAI delete process.env.CLAUDE_CODE_USE_GEMINI delete process.env.CLAUDE_CODE_USE_GITHUB @@ -374,6 +377,38 @@ test('Concentrate honors its legacy OpenAI model fallback before client normaliz expect(getMainLoopModel()).toBe('legacy-concentrate-model') }) +test('Concentrate skips a discovered-model rejection to its OpenAI fallback', async () => { + allowedModels = new Set(['legacy-concentrate-model']) + process.env.CONCENTRATE_API_KEY = 'concentrate-test' + process.env.CONCENTRATE_MODEL = 'rejected-concentrate-model' + process.env.OPENAI_MODEL = 'legacy-concentrate-model' + + const { + getDefaultMainLoopModelSetting, + getMainLoopModel, + getUserSpecifiedModelSetting, + } = await importFreshModelModule() + expect(getUserSpecifiedModelSetting()).toBe('legacy-concentrate-model') + expect(getDefaultMainLoopModelSetting()).toBe('legacy-concentrate-model') + expect(getMainLoopModel()).toBe('legacy-concentrate-model') +}) + +test('Concentrate falls back to its route default when configured models are rejected', async () => { + allowedModels = new Set() + process.env.CONCENTRATE_API_KEY = 'concentrate-test' + process.env.CONCENTRATE_MODEL = 'rejected-concentrate-model' + process.env.OPENAI_MODEL = 'also-rejected-model' + + const { + getDefaultMainLoopModelSetting, + getMainLoopModel, + getUserSpecifiedModelSetting, + } = await importFreshModelModule() + expect(getUserSpecifiedModelSetting()).toBeUndefined() + expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash-0731') + expect(getMainLoopModel()).toBe('deepseek-v4-flash-0731') +}) + test('Concentrate uses its descriptor default before client normalization', async () => { process.env.CONCENTRATE_API_KEY = 'concentrate-test' diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 12b32bf075..bb086a191c 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -58,6 +58,17 @@ function getUsableProviderConfigModel(value: string | undefined): string | undef return lowercase === 'undefined' || lowercase === 'null' ? undefined : normalized } +function getAllowedConcentrateConfigModel(): string | undefined { + for (const value of [ + process.env.CONCENTRATE_MODEL, + process.env.OPENAI_MODEL, + ]) { + const model = getUsableProviderConfigModel(value) + if (model && isModelAllowed(model)) return model + } + return undefined +} + export function getSmallFastModel(): ModelName { if (process.env.ANTHROPIC_SMALL_FAST_MODEL) return process.env.ANTHROPIC_SMALL_FAST_MODEL if (isCustomAnthropicProvider()) { @@ -161,8 +172,7 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined { // happens before the client applies its OpenAI-compatible defaults, so // consume its dedicated setting here rather than falling through to a // saved model from an unrelated provider. - ? getUsableProviderConfigModel(process.env.CONCENTRATE_MODEL) || - getUsableProviderConfigModel(process.env.OPENAI_MODEL) + ? getAllowedConcentrateConfigModel() : (provider === 'gemini' ? process.env.GEMINI_MODEL : undefined) || (provider === 'mistral' ? process.env.MISTRAL_MODEL : undefined) || (provider === 'minimax' ? getMiniMaxModelEnv() : undefined) || @@ -389,8 +399,7 @@ export function getRuntimeMainLoopModel(params: { export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { if (resolveActiveRouteIdFromEnv(process.env) === 'concentrate') { return ( - getUsableProviderConfigModel(process.env.CONCENTRATE_MODEL) || - getUsableProviderConfigModel(process.env.OPENAI_MODEL) || + getAllowedConcentrateConfigModel() || getRouteDefaultModel('concentrate') || 'deepseek-v4-flash-0731' ) diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index 9816371196..32027c0080 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -248,6 +248,17 @@ test('noncanonical CONCENTRATE_BASE_URL is rejected instead of silently withhold ) }) +test('key-only noncanonical Concentrate setup validates before OpenAI mode is enabled', async () => { + process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/staging/v1' + process.env.CONCENTRATE_API_KEY = 'concentrate-key' + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_API_KEYS + + await expect(getProviderValidationError(process.env)).resolves.toBe( + 'Concentrate credentials require the canonical https://api.concentrate.ai/v1 endpoint.', + ) +}) + test('Concentrate key-only setup validates before client defaults are applied', async () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.CONCENTRATE_API_KEY = 'concentrate-key' diff --git a/src/utils/providerValidation.ts b/src/utils/providerValidation.ts index a5ac167df1..6f4f2ed440 100644 --- a/src/utils/providerValidation.ts +++ b/src/utils/providerValidation.ts @@ -263,18 +263,18 @@ function getRuntimeValidationTarget( return enabledTarget } - if (!useOpenAI) { - return undefined - } - // The documented CONCENTRATE_API_KEY-only setup is routed before the client // applies its default base URL. Select its descriptor directly so startup - // validates the dedicated credential instead of falling through to generic - // OpenAI validation. + // validates the dedicated credential, including a noncanonical dedicated + // base URL, instead of returning early for an unset OpenAI mode. if (resolveActiveRouteIdFromEnv(env) === 'concentrate') { return validationTargets.find(target => target.descriptor.id === 'concentrate') } + if (!useOpenAI) { + return undefined + } + const request = resolveProviderRequest({ model: env.OPENAI_MODEL, baseUrl: env.OPENAI_BASE_URL, From 48243f38ac13af879fad80a02d431837f1a05670 Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 15:41:43 -0700 Subject: [PATCH 18/19] fix(concentrate): reset stale route state --- .env.example | 2 +- README.md | 2 +- src/integrations/gateways/concentrate.test.ts | 20 +++-- src/integrations/gateways/concentrate.ts | 11 ++- src/integrations/routeMetadata.test.ts | 25 ++++-- src/integrations/routeMetadata.ts | 12 ++- src/services/api/client.test.ts | 10 +-- src/services/api/client.ts | 12 ++- .../api/openaiShim/requestExecutor.test.ts | 23 +++++ .../api/openaiShim/requestExecutor.ts | 1 + .../model/model.openai-shim-providers.test.ts | 10 +-- src/utils/model/model.ts | 2 +- src/utils/providerFlag.test.ts | 29 ++++++- src/utils/providerFlag.ts | 85 +++++++++++++------ src/utils/providerProfile.test.ts | 34 +++++++- src/utils/providerProfile.ts | 4 +- src/utils/providerProfiles.test.ts | 21 +++++ src/utils/providerProfiles.ts | 4 +- src/utils/providerValidation.test.ts | 11 ++- 19 files changed, 235 insertions(+), 83 deletions(-) diff --git a/.env.example b/.env.example index 2e747ef21d..a6db870499 100644 --- a/.env.example +++ b/.env.example @@ -421,7 +421,7 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here # CLAUDE_CODE_USE_OPENAI=1 # CONCENTRATE_API_KEY=sk-cn-your-key-here # CONCENTRATE_BASE_URL=https://api.concentrate.ai/v1 (optional, default) -# CONCENTRATE_MODEL=deepseek-v4-flash-0731 (optional, default) +# CONCENTRATE_MODEL=deepseek-v4-flash (optional, default) # # Or configure interactively with /provider diff --git a/README.md b/README.md index 01187113e8..1efee55f6e 100644 --- a/README.md +++ b/README.md @@ -290,7 +290,7 @@ Advanced and source-build guides: | OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers | | Z.AI GLM Coding Plan | `/provider` or OpenAI-compatible env vars | Uses `OPENAI_API_KEY` at `https://api.z.ai/api/coding/paas/v4` and defaults to `glm-5.2` | | AI/ML API | `/provider` or `AIMLAPI_API_KEY` ([setup guide](docs/aimlapi-setup.md)) | Uses `https://api.aimlapi.com/v1`, auto-detects the OpenAI-compatible route from `AIMLAPI_API_KEY`, sends OpenClaude attribution headers, and discovers chat-capable models from the public `/models` catalog | -| Concentrate | `/provider` or `CONCENTRATE_API_KEY` | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash-0731` and auto-discovers the chat model catalog | +| Concentrate | `/provider` or `CONCENTRATE_API_KEY` | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash` and auto-discovers the chat model catalog | | ApiSmart | `/provider` or `APISMART_API_KEY` | Uses `https://gw.apismart.ai/v1`, defaults to `DEEPSEEK_V4_FLASH`, and supports optional `APISMART_MODEL` plus authenticated model discovery | | Hicap | `/provider` or OpenAI-compatible env vars | Uses `api-key` auth, discovers models from unauthenticated `/models`, and supports Responses mode for `gpt-` models | | Fireworks AI | `/provider` or env vars | First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses `FIREWORKS_API_KEY` | diff --git a/src/integrations/gateways/concentrate.test.ts b/src/integrations/gateways/concentrate.test.ts index 55c9261e20..963c90ac9c 100644 --- a/src/integrations/gateways/concentrate.test.ts +++ b/src/integrations/gateways/concentrate.test.ts @@ -10,13 +10,13 @@ describe('concentrate gateway', () => { expect(concentrate.label).toBe('Concentrate') expect(concentrate.category).toBe('aggregating') expect(concentrate.defaultBaseUrl).toBe('https://api.concentrate.ai/v1') - expect(concentrate.defaultModel).toBe('deepseek-v4-flash-0731') + expect(concentrate.defaultModel).toBe('deepseek-v4-flash') expect(concentrate.supportsModelRouting).toBe(true) expect(concentrate.setup.requiresAuth).toBe(true) expect(concentrate.setup.authMode).toBe('api-key') expect(concentrate.setup.credentialEnvVars).toEqual(['CONCENTRATE_API_KEY']) - expect(concentrate.setup.dedicatedCredentialsOnly).toBe(true) + expect(concentrate.setup.dedicatedCredentialsOnly).toBeUndefined() expect(concentrate.catalog?.source).toBe('dynamic') expect(concentrate.catalog?.discovery?.kind).toBe('openai-compatible') @@ -25,7 +25,11 @@ describe('concentrate gateway', () => { expect(concentrate.validation?.kind).toBe('credential-env') expect( (concentrate.validation as { credentialEnvVars: string[] }).credentialEnvVars, - ).toEqual(['CONCENTRATE_API_KEY']) + ).toEqual([ + 'CONCENTRATE_API_KEY', + 'OPENAI_API_KEYS', + 'OPENAI_API_KEY', + ]) expect(mapModel).toBeDefined() }) @@ -35,16 +39,16 @@ describe('concentrate gateway', () => { expect( mapModel({ - id: 'deepseek-v4-flash-0731', - display_name: 'DeepSeek V4 Flash 0731', + id: 'deepseek-v4-flash', + display_name: 'DeepSeek V4 Flash', owned_by: 'deepseek', max_input_tokens: 1_048_576, max_tokens: 393_216, }), ).toEqual({ - id: 'deepseek-v4-flash-0731', - apiName: 'deepseek-v4-flash-0731', - label: 'DeepSeek V4 Flash 0731', + id: 'deepseek-v4-flash', + apiName: 'deepseek-v4-flash', + label: 'DeepSeek V4 Flash', contextWindow: 1_048_576, maxOutputTokens: 393_216, }) diff --git a/src/integrations/gateways/concentrate.ts b/src/integrations/gateways/concentrate.ts index 9522860270..fd389eb1fb 100644 --- a/src/integrations/gateways/concentrate.ts +++ b/src/integrations/gateways/concentrate.ts @@ -56,13 +56,12 @@ export default defineGateway({ label: 'Concentrate', category: 'aggregating', defaultBaseUrl: 'https://api.concentrate.ai/v1', - defaultModel: 'deepseek-v4-flash-0731', + defaultModel: 'deepseek-v4-flash', supportsModelRouting: true, setup: { requiresAuth: true, authMode: 'api-key', credentialEnvVars: ['CONCENTRATE_API_KEY'], - dedicatedCredentialsOnly: true, }, startup: { probeReadiness: 'openai-compatible-models', @@ -89,9 +88,13 @@ export default defineGateway({ matchDefaultBaseUrl: true, matchBaseUrlHosts: ['api.concentrate.ai'], }, - credentialEnvVars: ['CONCENTRATE_API_KEY'], + credentialEnvVars: [ + 'CONCENTRATE_API_KEY', + 'OPENAI_API_KEYS', + 'OPENAI_API_KEY', + ], missingCredentialMessage: - 'Concentrate auth is required. Set CONCENTRATE_API_KEY.', + 'Concentrate auth is required. Set CONCENTRATE_API_KEY or OPENAI_API_KEY.', }, catalog: { source: 'dynamic', diff --git a/src/integrations/routeMetadata.test.ts b/src/integrations/routeMetadata.test.ts index abf722cf58..42c399cc1a 100644 --- a/src/integrations/routeMetadata.test.ts +++ b/src/integrations/routeMetadata.test.ts @@ -1028,7 +1028,7 @@ test('resolveActiveRouteIdFromEnv treats Concentrate credential-only env as Conc ).toBe('concentrate') }) -test('resolveActiveRouteIdFromEnv treats CONCENTRATE_BASE_URL as Concentrate intent', () => { +test('resolveActiveRouteIdFromEnv uses CONCENTRATE_BASE_URL with a dedicated credential', () => { expect( resolveActiveRouteIdFromEnv({ CONCENTRATE_API_KEY: 'concentrate-key', @@ -1037,7 +1037,7 @@ test('resolveActiveRouteIdFromEnv treats CONCENTRATE_BASE_URL as Concentrate int ).toBe('concentrate') }) -test('resolveActiveRouteIdFromEnv treats CONCENTRATE_MODEL as Concentrate intent', () => { +test('resolveActiveRouteIdFromEnv uses CONCENTRATE_MODEL with a dedicated credential', () => { expect( resolveActiveRouteIdFromEnv({ CONCENTRATE_API_KEY: 'concentrate-key', @@ -1114,13 +1114,17 @@ test('resolveActiveRouteIdFromEnv refines generic OpenAI profile by Concentrate ).toBe('concentrate') }) -test('getRouteCredentialEnvVars omits generic OpenAI fallback for Concentrate dedicated route', () => { - expect(getRouteCredentialEnvVars('concentrate')).toEqual(['CONCENTRATE_API_KEY']) +test('getRouteCredentialEnvVars supports documented generic OpenAI Concentrate setup', () => { + expect(getRouteCredentialEnvVars('concentrate')).toEqual([ + 'CONCENTRATE_API_KEY', + 'OPENAI_API_KEYS', + 'OPENAI_API_KEY', + ]) expect( getRouteCredentialValue('concentrate', { OPENAI_API_KEY: 'generic-openai-key', }), - ).toBeUndefined() + ).toBe('generic-openai-key') expect( getRouteCredentialValue('concentrate', { OPENAI_API_KEY: 'generic-openai-key', @@ -1128,3 +1132,14 @@ test('getRouteCredentialEnvVars omits generic OpenAI fallback for Concentrate de }), ).toBe('concentrate-key') }) + +test('resolveActiveRouteIdFromEnv does not let a stale Concentrate model override generic OpenAI', () => { + expect( + resolveActiveRouteIdFromEnv({ + CLAUDE_CODE_USE_OPENAI: '1', + OPENAI_BASE_URL: 'https://api.openai.com/v1', + OPENAI_API_KEY: 'generic-openai-key', + CONCENTRATE_MODEL: 'deepseek-v4-flash-0731', + }), + ).toBe('openai') +}) diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index 26457b387b..bba8f8dab9 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -954,14 +954,12 @@ export function hasApismartEnvOnlyProviderIntent( export function hasConcentrateEnvOnlyProviderIntent( processEnv: NodeJS.ProcessEnv = process.env, ): boolean { - // Concentrate is a dedicated-credentials-only OpenAI-compatible gateway. - // The documented CONCENTRATE_* env vars select the route even when a generic - // OPENAI_API_KEY is present in the shell, so the dedicated credential is the - // only key forwarded to api.concentrate.ai. + // The dedicated credential explicitly selects Concentrate. Base/model + // overrides alone are configuration details, not route identity: treating + // them as identity would let a stale optional setting override a valid + // generic OpenAI configuration. return ( - (hasUsableOpenAICredential(processEnv.CONCENTRATE_API_KEY) || - hasNonEmptyEnvValue(processEnv.CONCENTRATE_BASE_URL) || - hasNonEmptyEnvValue(processEnv.CONCENTRATE_MODEL)) && + hasUsableOpenAICredential(processEnv.CONCENTRATE_API_KEY) && !hasConflictingOpenAIBaseUrlForRoute(processEnv, isConcentrateBaseUrl) && !(processEnv.CLAUDE_CODE_USE_OPENAI !== undefined && !isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) && diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts index 6b6576e36d..42237c65b7 100644 --- a/src/services/api/client.test.ts +++ b/src/services/api/client.test.ts @@ -875,22 +875,22 @@ test('env-only Concentrate setup withholds its key from a noncanonical same-host expect(process.env.OPENAI_API_KEY).toBeUndefined() }) -test('env-only Concentrate does not forward an ambient generic OpenAI key', async () => { +test('generic OpenAI configuration for the canonical Concentrate endpoint retains its key', async () => { delete process.env.CLAUDE_CODE_USE_GEMINI delete process.env.GEMINI_API_KEY delete process.env.GEMINI_MODEL delete process.env.GEMINI_BASE_URL delete process.env.GEMINI_AUTH_MODE process.env.OPENAI_API_KEY = 'generic-openai-key' - process.env.CONCENTRATE_BASE_URL = 'https://api.concentrate.ai/v1' - process.env.CONCENTRATE_MODEL = 'claude-sonnet-5' + process.env.OPENAI_BASE_URL = 'https://api.concentrate.ai/v1' + process.env.OPENAI_MODEL = 'claude-sonnet-5' await getAnthropicClient({ maxRetries: 0, model: 'claude-sonnet-5' }) - expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1') + expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined() expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/v1') expect(process.env.OPENAI_MODEL).toBe('claude-sonnet-5') - expect(process.env.OPENAI_API_KEY).toBeUndefined() + expect(process.env.OPENAI_API_KEY).toBe('generic-openai-key') }) test('routes env-only AI/ML API requests through the OpenAI-compatible shim despite an ambient OpenAI key', async () => { diff --git a/src/services/api/client.ts b/src/services/api/client.ts index 1b48d7d071..42cb530651 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -428,13 +428,11 @@ function applyConcentrateEnvOnlyDefaults(): void { process.env.OPENAI_BASE_URL = baseUrlOverride ?? getRouteDefaultBaseUrl('concentrate') process.env.OPENAI_MODEL = modelOverride ?? getRouteDefaultModel('concentrate') - // Concentrate is dedicatedCredentialsOnly: only CONCENTRATE_API_KEY may - // authenticate this route. Mirror it into OPENAI_API_KEY for the shared - // OpenAI-compatible transport after the route identity is established, and - // clear any stale generic key so ambient OpenAI credentials are not - // forwarded to Concentrate. The dedicated key belongs only to Concentrate's - // documented /v1 inference endpoint, so a noncanonical same-host URL withholds - // the credential the same way ApiSmart does. + // A dedicated key explicitly selects Concentrate. Mirror it into + // OPENAI_API_KEY for the shared transport and avoid forwarding a stale + // generic credential. Generic OpenAI credentials remain supported when the + // user explicitly configures the canonical Concentrate base URL without the + // dedicated selection key. if ( hasUsableOpenAICredential(apiKey) && isCanonicalConcentrateInferenceBaseUrl(process.env.OPENAI_BASE_URL) diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index 1f9d06801b..f41cb6d107 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -50,6 +50,9 @@ const originalEnv = { OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY, DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY, MIMO_API_KEY: process.env.MIMO_API_KEY, + CONCENTRATE_API_KEY: process.env.CONCENTRATE_API_KEY, + CONCENTRATE_BASE_URL: process.env.CONCENTRATE_BASE_URL, + CONCENTRATE_MODEL: process.env.CONCENTRATE_MODEL, OPENGATEWAY_API_KEY: process.env.OPENGATEWAY_API_KEY, OPENGATEWAY_BASE_URL: process.env.OPENGATEWAY_BASE_URL, OPENCODE_API_KEY: process.env.OPENCODE_API_KEY, @@ -471,6 +474,9 @@ beforeEach(async () => { delete process.env.OPENROUTER_API_KEY delete process.env.DEEPSEEK_API_KEY delete process.env.MIMO_API_KEY + delete process.env.CONCENTRATE_API_KEY + delete process.env.CONCENTRATE_BASE_URL + delete process.env.CONCENTRATE_MODEL delete process.env.OPENGATEWAY_API_KEY delete process.env.OPENGATEWAY_BASE_URL delete process.env.OPENCODE_API_KEY @@ -517,6 +523,9 @@ afterEach(() => { restoreEnv('OPENROUTER_API_KEY', originalEnv.OPENROUTER_API_KEY) restoreEnv('DEEPSEEK_API_KEY', originalEnv.DEEPSEEK_API_KEY) restoreEnv('MIMO_API_KEY', originalEnv.MIMO_API_KEY) + restoreEnv('CONCENTRATE_API_KEY', originalEnv.CONCENTRATE_API_KEY) + restoreEnv('CONCENTRATE_BASE_URL', originalEnv.CONCENTRATE_BASE_URL) + restoreEnv('CONCENTRATE_MODEL', originalEnv.CONCENTRATE_MODEL) restoreEnv('OPENGATEWAY_API_KEY', originalEnv.OPENGATEWAY_API_KEY) restoreEnv('OPENGATEWAY_BASE_URL', originalEnv.OPENGATEWAY_BASE_URL) restoreEnv('OPENCODE_API_KEY', originalEnv.OPENCODE_API_KEY) @@ -547,6 +556,20 @@ test('gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic expect(captured.authorization).toBe('Bearer fake-ogw-key') }) +test('Concentrate selection prefers its dedicated key over a generic OPENAI_API_KEYS pool', async () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-key' + process.env.OPENAI_API_KEYS = 'generic-openai-key-a,generic-openai-key-b' + delete process.env.OPENAI_API_KEY + + const result = applyProviderFlag('concentrate', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('https://api.concentrate.ai/v1/chat/completions') + expect(captured.authorization).toBe('Bearer concentrate-key') +}) + test('gitlawb opengateway provider flag uses generic OPENAI_API_KEYS pool before generic OPENAI_API_KEY fallback', async () => { process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' process.env.OPENAI_API_KEYS = 'fake-openai-pool-a,fake-openai-pool-b' diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 08aa05d720..a1ca2e819f 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -288,6 +288,7 @@ export async function executeOpenAIRequest( requestProcessEnv.MINIMAX_API_KEY, requestProcessEnv.ATLAS_CLOUD_API_KEY, requestProcessEnv.APISMART_API_KEY, + requestProcessEnv.CONCENTRATE_API_KEY, requestProcessEnv.NEARAI_API_KEY, requestProcessEnv.FIREWORKS_API_KEY, requestProcessEnv.LONGCAT_API_KEY, diff --git a/src/utils/model/model.openai-shim-providers.test.ts b/src/utils/model/model.openai-shim-providers.test.ts index a0ea8ca488..9ce00d8423 100644 --- a/src/utils/model/model.openai-shim-providers.test.ts +++ b/src/utils/model/model.openai-shim-providers.test.ts @@ -405,15 +405,15 @@ test('Concentrate falls back to its route default when configured models are rej getUserSpecifiedModelSetting, } = await importFreshModelModule() expect(getUserSpecifiedModelSetting()).toBeUndefined() - expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash-0731') - expect(getMainLoopModel()).toBe('deepseek-v4-flash-0731') + expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash') + expect(getMainLoopModel()).toBe('deepseek-v4-flash') }) test('Concentrate uses its descriptor default before client normalization', async () => { process.env.CONCENTRATE_API_KEY = 'concentrate-test' const { getDefaultMainLoopModelSetting } = await importFreshModelModule() - expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash-0731') + expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash') }) test.each(['null', 'undefined', ' '])( @@ -429,8 +429,8 @@ test.each(['null', 'undefined', ' '])( getUserSpecifiedModelSetting, } = await importFreshModelModule() expect(getUserSpecifiedModelSetting()).toBeUndefined() - expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash-0731') - expect(getMainLoopModel()).toBe('deepseek-v4-flash-0731') + expect(getDefaultMainLoopModelSetting()).toBe('deepseek-v4-flash') + expect(getMainLoopModel()).toBe('deepseek-v4-flash') }, ) diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index bb086a191c..84119809e6 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -401,7 +401,7 @@ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { return ( getAllowedConcentrateConfigModel() || getRouteDefaultModel('concentrate') || - 'deepseek-v4-flash-0731' + 'deepseek-v4-flash' ) } // Custom Anthropic-compatible endpoints intentionally retain the legacy diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index aca9e1f93c..b94a995c18 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -13,6 +13,7 @@ import { applyModelFlagFromArgs, VALID_PROVIDERS, } from './providerFlag.js' +import { resolveActiveRouteIdFromEnv } from '../integrations/routeMetadata.js' const ENV_KEYS = [ 'CLAUDE_CODE_USE_OPENAI', @@ -1360,7 +1361,7 @@ describe('applyProviderFlag - concentrate', () => { expect(result.error).toBeUndefined() expect(process.env.CLAUDE_CODE_USE_OPENAI).toBe('1') expect(process.env.OPENAI_BASE_URL).toBe('https://api.concentrate.ai/v1') - expect(process.env.OPENAI_MODEL).toBe('deepseek-v4-flash-0731') + expect(process.env.OPENAI_MODEL).toBe('deepseek-v4-flash') expect(process.env.OPENAI_API_KEY).toBe('concentrate-secret-key') }) @@ -1392,6 +1393,7 @@ describe('applyProviderFlag - concentrate', () => { applyProviderFlag('concentrate', ['--model', 'deepseek-v4-pro-0731']) expect(process.env.OPENAI_MODEL).toBe('deepseek-v4-pro-0731') + expect(process.env.CONCENTRATE_MODEL).toBeUndefined() }) test('dedicated key overrides a lingering OPENAI_API_KEY from another provider', () => { @@ -1432,6 +1434,31 @@ describe('applyProviderFlag - concentrate', () => { expect(process.env.OPENAI_API_KEY).toBeUndefined() }) + test('switching from Concentrate to Ollama replaces the route identity and credentials', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' + + applyProviderFlag('concentrate', []) + applyProviderFlag('ollama', []) + + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + expect(process.env.CONCENTRATE_BASE_URL).toBeUndefined() + expect(process.env.CONCENTRATE_MODEL).toBeUndefined() + expect(process.env.OPENAI_BASE_URL).toBe('http://localhost:11434/v1') + expect(process.env.OPENAI_API_KEY).toBe('ollama') + expect(resolveActiveRouteIdFromEnv(process.env)).toBe('ollama') + }) + + test('switching from Concentrate to OpenAI replaces the known gateway endpoint', () => { + process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' + + applyProviderFlag('concentrate', []) + applyProviderFlag('openai', []) + + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + expect(process.env.OPENAI_BASE_URL).toBe('https://api.openai.com/v1') + expect(resolveActiveRouteIdFromEnv(process.env)).toBe('openai') + }) + test('clears unsupported OpenAI shim settings from a previous route', () => { process.env.CONCENTRATE_API_KEY = 'concentrate-secret-key' process.env.OPENAI_API_FORMAT = 'responses' diff --git a/src/utils/providerFlag.ts b/src/utils/providerFlag.ts index 67eb3a8713..e7ab51bade 100644 --- a/src/utils/providerFlag.ts +++ b/src/utils/providerFlag.ts @@ -206,7 +206,6 @@ function shouldReplaceStaleKnownBaseUrl(provider: string): boolean { const targetRouteId = resolveProfileRoute(provider).routeId return ( - targetRouteId !== 'openai' && targetRouteId !== 'custom' && targetRouteId !== 'unknown-fallback' && currentRouteId !== targetRouteId @@ -255,6 +254,12 @@ function clearUnsupportedOpenAIShimSettings(routeId: string): void { } } +function clearConcentrateProviderState(): void { + delete process.env.CONCENTRATE_API_KEY + delete process.env.CONCENTRATE_BASE_URL + delete process.env.CONCENTRATE_MODEL +} + function usableProviderModelEnvValue( value: string | undefined, ): string | undefined { @@ -418,9 +423,6 @@ export function applyProviderFlag( // OpenAI-compatible env-only route override it later in startup. delete process.env.APISMART_API_KEY delete process.env.APISMART_MODEL - delete process.env.CONCENTRATE_API_KEY - delete process.env.CONCENTRATE_BASE_URL - delete process.env.CONCENTRATE_MODEL delete process.env.ANTHROPIC_AUTH_TOKEN delete process.env.ANTHROPIC_CUSTOM_HEADERS break @@ -457,9 +459,6 @@ export function applyProviderFlag( delete process.env.OPENAI_AUTH_HEADER delete process.env.OPENAI_AUTH_SCHEME delete process.env.OPENAI_AUTH_HEADER_VALUE - delete process.env.CONCENTRATE_API_KEY - delete process.env.CONCENTRATE_BASE_URL - delete process.env.CONCENTRATE_MODEL process.env.ANTHROPIC_MODEL ??= defaultModel if (model) process.env.ANTHROPIC_MODEL = model break @@ -467,12 +466,11 @@ export function applyProviderFlag( case 'openai': process.env.CLAUDE_CODE_USE_OPENAI = '1' // An explicit generic OpenAI selection must not be reclassified as a - // dedicated env-only gateway during client startup. + // dedicated env-only gateway during client startup. Replace a previous + // known gateway endpoint, but preserve a user-supplied custom endpoint. delete process.env.APISMART_API_KEY delete process.env.APISMART_MODEL - delete process.env.CONCENTRATE_API_KEY - delete process.env.CONCENTRATE_BASE_URL - delete process.env.CONCENTRATE_MODEL + applyOpenAIBaseUrlDefault(provider, defaultBaseUrl) if (model) process.env.OPENAI_MODEL = model break @@ -501,7 +499,10 @@ export function applyProviderFlag( case 'ollama': process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL ??= defaultBaseUrl ?? 'http://localhost:11434/v1' + applyOpenAIBaseUrlDefault( + provider, + defaultBaseUrl ?? 'http://localhost:11434/v1', + ) if (!process.env.OPENAI_API_KEY) { process.env.OPENAI_API_KEY = 'ollama' } @@ -510,7 +511,10 @@ export function applyProviderFlag( case 'nvidia-nim': process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL ??= defaultBaseUrl ?? 'https://integrate.api.nvidia.com/v1' + applyOpenAIBaseUrlDefault( + provider, + defaultBaseUrl ?? 'https://integrate.api.nvidia.com/v1', + ) process.env.NVIDIA_NIM = '1' if (process.env.NVIDIA_API_KEY && !process.env.OPENAI_API_KEY) { process.env.OPENAI_API_KEY = process.env.NVIDIA_API_KEY @@ -521,7 +525,10 @@ export function applyProviderFlag( case 'bankr': process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL ??= defaultBaseUrl ?? 'https://llm.bankr.bot/v1' + applyOpenAIBaseUrlDefault( + provider, + defaultBaseUrl ?? 'https://llm.bankr.bot/v1', + ) process.env.OPENAI_MODEL ??= 'claude-opus-4.6' if (model) process.env.OPENAI_MODEL = model if (process.env.BNKR_API_KEY && !process.env.OPENAI_API_KEY) { @@ -582,7 +589,7 @@ export function applyProviderFlag( case 'xai': process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL ??= 'https://api.x.ai/v1' + applyOpenAIBaseUrlDefault(provider, defaultBaseUrl ?? 'https://api.x.ai/v1') process.env.OPENAI_MODEL ??= defaultModel ?? 'grok-4.6' if (model) process.env.OPENAI_MODEL = model if (process.env.XAI_API_KEY && !process.env.OPENAI_API_KEY) { @@ -592,7 +599,10 @@ export function applyProviderFlag( case 'xiaomi-mimo': process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL ??= defaultBaseUrl ?? 'https://api.xiaomimimo.com/v1' + applyOpenAIBaseUrlDefault( + provider, + defaultBaseUrl ?? 'https://api.xiaomimimo.com/v1', + ) process.env.OPENAI_MODEL ??= defaultModel ?? 'mimo-v2.5-pro' if (model) process.env.OPENAI_MODEL = model if (process.env.MIMO_API_KEY && !process.env.OPENAI_API_KEY) { @@ -615,7 +625,10 @@ export function applyProviderFlag( case 'venice': process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL ??= defaultBaseUrl ?? 'https://api.venice.ai/api/v1' + applyOpenAIBaseUrlDefault( + provider, + defaultBaseUrl ?? 'https://api.venice.ai/api/v1', + ) process.env.OPENAI_MODEL ??= defaultModel ?? 'venice-uncensored' if (model) process.env.OPENAI_MODEL = model if (process.env.VENICE_API_KEY && !process.env.OPENAI_API_KEY) { @@ -698,10 +711,12 @@ export function applyProviderFlag( if (baseUrlOverride) { process.env.OPENAI_BASE_URL = baseUrlOverride } else { - applyOpenAIBaseUrlDefault( - provider, - defaultBaseUrl ?? 'https://api.concentrate.ai/v1', - ) + // An explicit Concentrate selection must not retain an unrelated + // custom OpenAI endpoint from the preceding provider. Users can + // deliberately select a different Concentrate endpoint through + // CONCENTRATE_BASE_URL above. + process.env.OPENAI_BASE_URL = + defaultBaseUrl ?? 'https://api.concentrate.ai/v1' } } { @@ -714,18 +729,21 @@ export function applyProviderFlag( process.env.OPENAI_MODEL ??= usableProviderModelEnvValue(process.env.OPENAI_MODEL) || defaultModel || - 'deepseek-v4-flash-0731' + 'deepseek-v4-flash' } } if (model) { + // Runtime model resolution gives CONCENTRATE_MODEL priority over the + // shared shim setting. An explicit CLI selection must supersede that + // ambient provider default all the way through client normalization. + delete process.env.CONCENTRATE_MODEL process.env.OPENAI_MODEL = model } - // DedicatedCredentialsOnly: only CONCENTRATE_API_KEY authenticates this - // route. Mirror it into OPENAI_API_KEY for the shared shim transport and - // clear any stale generic key so ambient OpenAI credentials are never - // forwarded to Concentrate. A Concentrate key is only valid for its - // documented inference endpoint; do not mirror it to a same-host proxy - // or alternate path supplied through CONCENTRATE_BASE_URL. + // This explicit dedicated selection mirrors CONCENTRATE_API_KEY into the + // shared shim transport and clears a stale generic key. A generic + // OpenAI-compatible Concentrate setup remains supported when selected + // through OPENAI_BASE_URL instead. Do not mirror the dedicated key to a + // same-host proxy or alternate path supplied through CONCENTRATE_BASE_URL. if ( hasUsableOpenAICredential(process.env.CONCENTRATE_API_KEY) && isCanonicalConcentrateInferenceBaseUrl(getConfiguredOpenAIBaseUrl()) @@ -826,5 +844,16 @@ export function applyProviderFlag( break } + // A provider flag selects a complete route for this process. Concentrate's + // dedicated variables are another source of route identity, so leaving them + // behind can re-select Concentrate after a later OpenAI-compatible provider + // has applied its defaults. Keep their lifecycle at the selection boundary, + // rather than relying on individual switch branches to remember cleanup. + // This runs only after the selected branch succeeds, so an invalid + // custom-anthropic request remains non-mutating. + if (provider !== 'concentrate') { + clearConcentrateProviderState() + } + return {} } diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index f1eb016294..2f252bc72a 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -3034,7 +3034,7 @@ test('openai launch preserves persisted Concentrate dedicated credentials across assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'concentrate') }) -test('openai launch backfills CONCENTRATE_API_KEY from a legacy OpenAI-shaped Concentrate profile', async () => { +test('legacy generic OpenAI Concentrate profiles keep their credential generic', async () => { const env = await buildLaunchEnv({ profile: 'openai', persisted: profile('openai', { @@ -3048,7 +3048,7 @@ test('openai launch backfills CONCENTRATE_API_KEY from a legacy OpenAI-shaped Co assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'concentrate') assert.equal(env.OPENAI_API_KEY, 'concentrate-secret-key') - assert.equal(env.CONCENTRATE_API_KEY, 'concentrate-secret-key') + assert.equal(env.CONCENTRATE_API_KEY, undefined) assert.equal( resolveRouteCredentialValue({ routeId: 'concentrate', @@ -3059,7 +3059,7 @@ test('openai launch backfills CONCENTRATE_API_KEY from a legacy OpenAI-shaped Co ) }) -test('openai launch never promotes an ambient generic key to a Concentrate credential', async () => { +test('openai launch keeps a generic Concentrate credential generic', async () => { const env = await buildLaunchEnv({ profile: 'openai', persisted: profile('openai', { @@ -3080,7 +3080,33 @@ test('openai launch never promotes an ambient generic key to a Concentrate crede processEnv: env, baseUrl: env.OPENAI_BASE_URL, }), - undefined, + 'generic-openai-key', + ) +}) + +test('generic Concentrate profile lets a rotated OpenAI key override its saved key', async () => { + const env = await buildLaunchEnv({ + profile: 'openai', + persisted: profile('openai', { + OPENAI_BASE_URL: 'https://api.concentrate.ai/v1', + OPENAI_MODEL: 'deepseek-v4-flash-0731', + OPENAI_API_KEY: 'saved-generic-key', + }), + goal: 'coding', + processEnv: { + OPENAI_API_KEY: 'rotated-generic-key', + }, + }) + + assert.equal(env.OPENAI_API_KEY, 'rotated-generic-key') + assert.equal(env.CONCENTRATE_API_KEY, undefined) + assert.equal( + resolveRouteCredentialValue({ + routeId: 'concentrate', + processEnv: env, + baseUrl: env.OPENAI_BASE_URL, + }), + 'rotated-generic-key', ) }) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 41e7e18882..d4d6ec7f2a 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -994,9 +994,6 @@ export function buildOpenAIProfileEnv(options: { ...(resolveRouteIdFromBaseUrl(resolvedBaseUrl) === 'aimlapi' && key ? { AIMLAPI_API_KEY: key } : {}), - ...(isCanonicalConcentrateInferenceBaseUrl(resolvedBaseUrl) && key - ? { CONCENTRATE_API_KEY: key } - : {}), OPENAI_BASE_URL: resolvedBaseUrl, OPENAI_MODEL: normalizedModel, ...(options.apiFormat ? { OPENAI_API_FORMAT: options.apiFormat } : {}), @@ -2275,6 +2272,7 @@ export async function buildLaunchEnv(options: { const backfillLegacyConcentrateProfileKey = dedicatedKey === 'CONCENTRATE_API_KEY' && effectiveOpenAIRouteId === 'concentrate' && + persistedOpenAIRouteId === 'concentrate' && !!dedicatedBaseUrl && isCanonicalConcentrateInferenceBaseUrl(dedicatedBaseUrl) && persistedOpenAICredential?.kind === 'usable' diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index 324995e087..a26263d22f 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -985,6 +985,27 @@ describe('applyProviderProfileToProcessEnv', () => { expect(getFreshAPIProvider()).toBe('openai') }) + test('generic OpenAI profile at the canonical Concentrate endpoint keeps its credential generic', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + const { getProviderValidationError } = await import( + `./providerValidation.js?ts=${Date.now()}-${Math.random()}` + ) + + applyProviderProfileToProcessEnv( + buildProfile({ + provider: 'openai', + baseUrl: 'https://api.concentrate.ai/v1', + model: 'deepseek-v4-flash-0731', + apiKey: 'generic-concentrate-key', + }), + ) + + expect(process.env.OPENAI_API_KEY).toBe('generic-concentrate-key') + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + expect(await getProviderValidationError(process.env)).toBeNull() + }) + test('concentrate profile clears a stale route-specific model before applying its saved model', async () => { const { applyProviderProfileToProcessEnv } = await importFreshProviderProfileModules() diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index b675d92289..a73cfee035 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -1127,8 +1127,8 @@ export function applyProviderProfileToProcessEnv( } } } - // Concentrate is dedicatedCredentialsOnly: stamp the route identity and mirror - // the dedicated credential so the saved profile relaunches authenticated. + // Stamp dedicated Concentrate profile identity and mirror its credential so + // the saved profile relaunches authenticated. if (route.routeId === 'concentrate') { openAIProfileEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'concentrate' // Keyless canonical Concentrate profiles may resolve the ambient dedicated diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index 32027c0080..68337820e6 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -224,6 +224,15 @@ test('noncanonical Concentrate host paths fall back to generic OpenAI validation await expect(getProviderValidationError(process.env)).resolves.toBeNull() }) +test('canonical Concentrate base supports the documented generic OpenAI credential', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.concentrate.ai/v1' + process.env.OPENAI_API_KEY = 'generic-concentrate-key' + delete process.env.CONCENTRATE_API_KEY + + await expect(getProviderValidationError(process.env)).resolves.toBeNull() +}) + test('noncanonical same-host base rejects a selected CONCENTRATE_API_KEY route', async () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.OPENAI_BASE_URL = 'https://api.concentrate.ai/staging/v1' @@ -274,7 +283,7 @@ test.each(['SUA_CHAVE', 'sua_chave', 'null', 'undefined', ' NULL '])( process.env.CONCENTRATE_API_KEY = placeholder await expect(getProviderValidationError(process.env)).resolves.toBe( - 'Concentrate auth is required. Set CONCENTRATE_API_KEY.', + 'Concentrate auth is required. Set CONCENTRATE_API_KEY or OPENAI_API_KEY.', ) }, ) From 75aa0f2e5741f3ff3898abbe4b8d565681a9805a Mon Sep 17 00:00:00 2001 From: jatmn Date: Tue, 18 Aug 2026 18:11:16 -0700 Subject: [PATCH 19/19] fix(concentrate): preserve proxy profile capabilities --- src/utils/providerProfiles.test.ts | 39 ++++++++++++++++++++++++++++++ src/utils/providerProfiles.ts | 14 +++++++---- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index a26263d22f..d2d6f43cfa 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -1044,6 +1044,45 @@ describe('applyProviderProfileToProcessEnv', () => { expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('concentrate') }) + test('retargeted Concentrate profiles retain generic proxy capabilities', async () => { + const { addProviderProfile, applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + + const saved = addProviderProfile({ + provider: 'concentrate', + name: 'Concentrate proxy', + baseUrl: 'https://proxy.example/v1', + model: 'proxy-model', + apiKey: 'concentrate-test-key', + apiFormat: 'responses', + authHeader: 'X-Proxy-Key', + authScheme: 'raw', + authHeaderValue: 'proxy-auth-value', + customHeaders: { 'X-Proxy-Trace': 'enabled' }, + }) + + expect(saved).toMatchObject({ + apiFormat: 'responses', + authHeader: 'X-Proxy-Key', + authScheme: 'raw', + authHeaderValue: 'proxy-auth-value', + customHeaders: { 'X-Proxy-Trace': 'enabled' }, + }) + + applyProviderProfileToProcessEnv(saved!) + + expect(process.env.OPENAI_BASE_URL).toBe('https://proxy.example/v1') + expect(process.env.OPENAI_API_FORMAT).toBe('responses') + expect(process.env.OPENAI_AUTH_HEADER).toBe('X-Proxy-Key') + expect(process.env.OPENAI_AUTH_SCHEME).toBe('raw') + expect(process.env.OPENAI_AUTH_HEADER_VALUE).toBe('proxy-auth-value') + expect(process.env.ANTHROPIC_CUSTOM_HEADERS).toBe( + 'X-Proxy-Trace: enabled', + ) + expect(process.env.OPENAI_API_KEY).toBeUndefined() + expect(process.env.CONCENTRATE_API_KEY).toBeUndefined() + }) + test('keyless Concentrate profile resolves CONCENTRATE_API_KEY without persisting it', async () => { const { applyProviderProfileToProcessEnv } = await importFreshProviderProfileModules() diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index a73cfee035..52a73eacc4 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -253,15 +253,19 @@ function resolveProfileCapabilityRouteId( return routeIdFromBaseUrl } - // Cloudflare and LongCat profiles retargeted away from their dedicated - // endpoints run generically at runtime. Mirror that boundary here so - // capability-driven surfaces are not stripped based on stale route ids. + // Dedicated-route profiles retargeted away from their documented endpoints + // run generically at runtime. Mirror that boundary here so capability-driven + // surfaces are not stripped based on stale route ids. if ( - (providerRouteId === 'cloudflare' || providerRouteId === 'longcat') && + (providerRouteId === 'cloudflare' || + providerRouteId === 'longcat' || + providerRouteId === 'concentrate') && baseUrl && !(providerRouteId === 'cloudflare' ? isCloudflareBaseUrl(baseUrl) - : isLongcatBaseUrl(baseUrl)) + : providerRouteId === 'longcat' + ? isLongcatBaseUrl(baseUrl) + : isCanonicalConcentrateInferenceBaseUrl(baseUrl)) ) { return 'custom' }