feat: add OrcaRouter gateway descriptor (#2125)

* feat: add OrcaRouter gateway descriptor

Add first-class support for OrcaRouter, an OpenAI-compatible model routing
gateway, as a generated gateway descriptor with hybrid catalog discovery
and curated flagship models (openai/gpt-5.5, anthropic/claude-sonnet-4.6,
google/gemini-3.5-flash) plus its orcarouter/auto named router. Registers
ORCAROUTER_API_KEY in the env-file allow-list, surfaces the provider in the
web provider list and README, and updates the compatibility/registry tests
for the new preset.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: include OrcaRouter in provider picker navigation

* fix(orcarouter): preserve profile credentials and model limits

* fix(orcarouter): keep managed keys scoped to their profiles

* fix(orcarouter): preserve credential ownership across process boundaries

* fix: preserve Orca credential ownership across child startup

* fix(orcarouter): preserve credential provenance and discovery auth

* fix(orcarouter): preserve saved credential origin across launch paths

* test(orcarouter): restore credential origin in shim tests

* fix(orcarouter): preserve pane credential provenance

* docs(orcarouter): clarify dedicated-key migration

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jatmn <the@jat.mn>
This commit is contained in:
Marc-oss-hub
2026-10-26 07:21:15 -07:00
committed by GitHub
co-authored by Claude jatmn
parent cb4a72bf1c
commit 7b83353db1
26 changed files with 809 additions and 16 deletions
+1
View File
@@ -357,6 +357,7 @@ Advanced and source-build guides:
| 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 |
| API Route | `/provider` or `API_ROUTE_API_KEY` | Uses `https://global.api-route.com/v1`, defaults to `claude-sonnet-4-6`, and supports optional `API_ROUTE_MODEL` plus authenticated model discovery |
| GMI Cloud | `/provider`, `--provider gmi-cloud`, or env vars | Uses `GMI_API_KEY` at `https://api.gmi-serving.com/v1` and defaults to `Qwen/Qwen3.8-Max`; `--provider gmi-cloud` also reads an optional `GMI_MODEL`, and the model list comes from authenticated discovery |
| OrcaRouter | `/provider`, or `ORCAROUTER_API_KEY` with `--provider orcarouter` | OpenAI-compatible model routing gateway at `https://api.orcarouter.ai/v1`, defaults to `openai/gpt-5.5`, with `orcarouter/auto` smart routing and authenticated model discovery. Existing environment setups must replace `OPENAI_API_KEY` with `ORCAROUTER_API_KEY`, even when a custom authentication header supplies the request credential. Custom headers remain supported. |
| 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` |
| LongCat | `/provider` or env vars | Meituan LongCat OpenAI-compatible API at `https://api.longcat.chat/openai/v1`; uses `LONGCAT_API_KEY` and defaults to `LongCat-2.0` |
+1
View File
@@ -195,6 +195,7 @@ const PRESET_ORDER = [
'OpenCode Go',
'OpenCode Zen',
'OpenRouter',
'OrcaRouter',
'Together AI',
'Venice',
'xAI',
+1
View File
@@ -35,6 +35,7 @@ const EXPECTED_PRESETS = [
'merge-gateway',
'azure-openai',
'openrouter',
'orcarouter',
'lmstudio',
'dashscope-cn',
'dashscope-intl',
+73 -9
View File
@@ -1636,13 +1636,18 @@ const customAuthCases: Array<{
{ name: 'invalid header', value: undefined, scheme: undefined, expected: { authorization: 'Bearer pooled-key' } },
]
for (const auth of customAuthCases) {
test(`custom discovery auth ${auth.name}/${auth.scheme}/${auth.value}`, async () => {
test.each([
['custom', 'https://custom.example/v1'],
['orcarouter', 'https://api.orcarouter.ai/v1'],
] as const)(`%s discovery auth ${auth.name}/${auth.scheme}/${auth.value}`, async (routeId, baseUrl) => {
const { refreshStartupDiscoveryForActiveRoute, discoverModelsForRoute, getDiscoveryCacheKey } = await loadDiscoveryServiceModule()
const { resolveModelRuntimeLimits } = await import('./runtimeMetadata.js')
const env: NodeJS.ProcessEnv = {
CLAUDE_CODE_USE_OPENAI: '1',
OPENAI_BASE_URL: 'https://custom.example/v1',
OPENAI_API_KEYS: 'pooled-key,second-key',
OPENAI_BASE_URL: baseUrl,
...(routeId === 'custom'
? { OPENAI_API_KEYS: 'pooled-key,second-key' }
: { ORCAROUTER_API_KEY: 'pooled-key' }),
OPENAI_AUTH_HEADER: auth.name,
OPENAI_AUTH_HEADER_VALUE: auth.value,
OPENAI_AUTH_SCHEME: auth.scheme,
@@ -1660,22 +1665,24 @@ for (const auth of customAuthCases) {
new Headers(init?.headers).forEach((value, name) => { headers[name] = value })
requests.push({ url: String(input), headers })
return Promise.resolve(new Response(JSON.stringify({
data: [{ id: 'remote-model', context_window: 123456 }],
data: [{ id: 'remote-model', [routeId === 'custom' ? 'context_window' : 'context_length']: 123456 }],
}), { status: 200, headers: { 'Content-Type': 'application/json' } }))
}) as unknown as typeof globalThis.fetch)
const first = await refreshStartupDiscoveryForActiveRoute({ processEnv: env })
const first = routeId === 'custom'
? await refreshStartupDiscoveryForActiveRoute({ processEnv: env })
: await discoverModelsForRoute(routeId, { baseUrl, processEnv: env, headers: callerHeaders })
expect(first?.source).toBe('network')
expect(requests).toEqual([{ url: 'https://custom.example/v1/models', headers: { ...auth.expected, 'x-tenant': 'tenant-a' } }])
expect(requests).toEqual([{ url: `${baseUrl}/models`, headers: { ...auth.expected, 'x-tenant': 'tenant-a' } }])
// The picker supplies the resolved credential; startup supplies its env.
const cached = await discoverModelsForRoute('custom', {
const cached = await discoverModelsForRoute(routeId, {
baseUrl: env.OPENAI_BASE_URL, apiKey: 'pooled-key', processEnv: env, headers: callerHeaders,
})
expect(cached?.source).toBe('cache')
expect(requests).toHaveLength(1)
expect(resolveModelRuntimeLimits({ model: 'remote-model', processEnv: env }).contextWindow).toBe(123456)
const identityOptions = { baseUrl: env.OPENAI_BASE_URL, apiKey: 'pooled-key', processEnv: env }
expect(getDiscoveryCacheKey('custom', { ...identityOptions, headers: callerHeaders })).toBe(
getDiscoveryCacheKey('custom', { ...identityOptions, headers: { 'X-Tenant': 'tenant-a' } }),
expect(getDiscoveryCacheKey(routeId, { ...identityOptions, headers: callerHeaders })).toBe(
getDiscoveryCacheKey(routeId, { ...identityOptions, headers: { 'X-Tenant': 'tenant-a' } }),
)
// An explicit custom-auth value selects a different account/catalog.
if (auth.name === 'Authorization') {
@@ -1715,3 +1722,60 @@ test('custom startup discovery retains its auth when another profile activates',
},
}).contextWindow).toBe(123456)
})
test('Orca discovery preserves reported context through the cache and runtime', async () => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
const { resolveModelRuntimeLimits } = await import('./runtimeMetadata.js')
const env: NodeJS.ProcessEnv = {
CLAUDE_CODE_USE_OPENAI: '1', OPENAI_BASE_URL: 'https://api.orcarouter.ai/v1',
ORCAROUTER_API_KEY: 'orca-key',
}
const fetchMock = mock((_input: RequestInfo | URL, init?: RequestInit) => {
expect(new Headers(init?.headers).get('authorization')).toBe('Bearer orca-key')
return Promise.resolve(Response.json({ data: [
{ id: 'anthropic/claude-sonnet-4.6', context_length: 1_000_000 },
{ id: 'discovered-chat-model', context_length: 262_144 },
] }))
})
setMockFetch(fetchMock as unknown as typeof globalThis.fetch)
const options = { baseUrl: env.OPENAI_BASE_URL, apiKey: 'orca-key' }
expect((await discoverModelsForRoute('orcarouter', {
...options, forceRefresh: true,
}))?.source).toBe('network')
expect((await discoverModelsForRoute('orcarouter', options))?.source).toBe('cache')
expect(fetchMock).toHaveBeenCalledTimes(1)
for (const [model, contextWindow] of [
['anthropic/claude-sonnet-4.6', 1_000_000],
['discovered-chat-model', 262_144],
] as const) {
expect(resolveModelRuntimeLimits({ model, processEnv: env }).contextWindow)
.toBe(contextWindow)
}
})
// Capture the caller's identity before disk cache I/O, including the normal
// dedicated-key path when no explicit auth override exists.
test.each([undefined, 'profile-a-secret'])(
'Orca discovery retains launch identity across a profile change (override: %s)', async override => {
const { discoverModelsForRoute } = await loadDiscoveryServiceModule()
const { resolveModelRuntimeLimits } = await import('./runtimeMetadata.js')
const env: NodeJS.ProcessEnv = {
CLAUDE_CODE_USE_OPENAI: '1', OPENAI_BASE_URL: 'https://api.orcarouter.ai/v1',
ORCAROUTER_API_KEY: 'profile-a-key',
...(override ? { OPENAI_AUTH_HEADER: 'Authorization', OPENAI_AUTH_HEADER_VALUE: override } : {}),
}
const launchEnv = { ...env }
const headers: Array<string | null> = []
setMockFetch(mock((_input, init) => {
headers.push(new Headers(init?.headers).get('authorization'))
return Promise.resolve(Response.json({ data: [{ id: 'account-model', context_length: 123456 }] }))
}) as unknown as typeof globalThis.fetch)
const pending = discoverModelsForRoute('orcarouter', { processEnv: env, baseUrl: env.OPENAI_BASE_URL })
env.ORCAROUTER_API_KEY = 'profile-b-key'
if (override) env.OPENAI_AUTH_HEADER_VALUE = 'profile-b-secret'
expect((await pending)?.source).toBe('network')
expect(headers).toEqual([`Bearer ${override ?? 'profile-a-key'}`])
expect(resolveModelRuntimeLimits({ model: 'account-model', processEnv: launchEnv }).contextWindow).toBe(123456)
expect(resolveModelRuntimeLimits({ model: 'account-model', processEnv: env }).contextWindow).toBeUndefined()
},
)
+12 -6
View File
@@ -174,12 +174,18 @@ function getRouteBaseUrl(
return options?.baseUrl ?? getRouteDescriptor(routeId)?.defaultBaseUrl
}
// Keep custom endpoint authentication identical for requests and cache readers.
function supportsCustomDiscoveryAuth(routeId: string): boolean {
// Orca's named route preserves the custom authentication its endpoint
// supported before registration, including cache and in-flight identity.
return routeId === 'custom' || routeId === 'orcarouter'
}
// Keep configurable authentication identical for requests and cache readers.
function getCustomDiscoveryAuth(
routeId: string,
options?: { apiKey?: string; baseUrl?: string; processEnv?: NodeJS.ProcessEnv },
): Record<string, string> | undefined {
if (routeId !== 'custom') return undefined
if (!supportsCustomDiscoveryAuth(routeId)) return undefined
const env = options?.processEnv ?? process.env
const name = env.OPENAI_AUTH_HEADER?.trim()
if (!name || !/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(name)) return undefined
@@ -241,7 +247,7 @@ function getRouteDiscoveryApiKey(
routeId,
baseUrl,
processEnv:
routeId === 'custom' ? options?.processEnv ?? process.env : process.env,
supportsCustomDiscoveryAuth(routeId) ? options?.processEnv ?? process.env : process.env,
}),
)
}
@@ -260,7 +266,7 @@ export async function resolveDiscoveryRequestOptions<
resolverOptions?: { refreshXaiOAuth?: boolean },
): Promise<T> {
const next = { ...(options ?? {}) } as T
if (routeId === 'custom') {
if (supportsCustomDiscoveryAuth(routeId)) {
// Profile activation can change the global env while cache I/O is pending.
next.processEnv = { ...(next.processEnv ?? process.env) }
return next
@@ -548,7 +554,7 @@ export async function discoverModelsForRoute(
try {
const discoveryOptions = await resolveDiscoveryRequestOptions(
routeId,
routeId === 'custom' ? cachedOptions : options,
supportsCustomDiscoveryAuth(routeId) ? cachedOptions : options,
)
const discoveryCacheKey = getDiscoveryCacheKey(routeId, discoveryOptions)
const discovered = await runDiscovery(routeId, discoveryOptions)
@@ -635,7 +641,7 @@ export async function refreshStartupDiscoveryForRoute(
}
return discoverModelsForRoute(routeId, {
...(routeId === 'custom' ? cachedOptions : options),
...(supportsCustomDiscoveryAuth(routeId) ? cachedOptions : options),
forceRefresh: true,
})
}
@@ -0,0 +1,27 @@
import { describe, expect, test } from 'bun:test'
import orcarouter from './orcarouter.js'
describe('orcarouter gateway', () => {
test('uses hybrid discovery with dedicated credentials', () => {
expect(orcarouter.id).toBe('orcarouter')
expect(orcarouter.catalog?.source).toBe('hybrid')
expect(orcarouter.catalog?.discovery?.kind).toBe('openai-compatible')
expect(orcarouter.setup.dedicatedCredentialsOnly).toBe(true)
expect(orcarouter.setup.credentialEnvVars).toEqual(['ORCAROUTER_API_KEY'])
expect(orcarouter.defaultBaseUrl).toBe('https://api.orcarouter.ai/v1')
expect(orcarouter.defaultModel).toBe('openai/gpt-5.5')
})
test('curated catalog exposes auto routing plus flagship models', () => {
const models = orcarouter.catalog?.models ?? []
expect(models.some(model => model.apiName === 'orcarouter/auto')).toBe(true)
expect(models.some(model => model.apiName === 'openai/gpt-5.5')).toBe(true)
expect(
models.some(model => model.apiName === 'anthropic/claude-sonnet-4.6'),
).toBe(true)
expect(
models.some(model => model.apiName === 'google/gemini-3.5-flash'),
).toBe(true)
})
})
+83
View File
@@ -0,0 +1,83 @@
import { defineGateway } from '../define.js'
import { getTrimmedString, isRecord } from '../modelMapping.js'
function mapOrcarouterModel(raw: unknown) {
if (!isRecord(raw)) return null
const id = getTrimmedString(raw, 'id')
if (!id) return null
const context = raw.context_length
return {
id,
apiName: id,
label: id,
...(typeof context === 'number' && Number.isInteger(context) && context > 0
? { contextWindow: context }
: {}),
}
}
export default defineGateway({
id: 'orcarouter',
label: 'OrcaRouter',
category: 'aggregating',
defaultBaseUrl: 'https://api.orcarouter.ai/v1',
defaultModel: 'openai/gpt-5.5',
supportsModelRouting: true,
setup: {
requiresAuth: true,
authMode: 'api-key',
credentialEnvVars: ['ORCAROUTER_API_KEY'],
dedicatedCredentialsOnly: true,
},
startup: {
probeReadiness: 'openai-compatible-models',
},
transportConfig: {
kind: 'openai-compatible',
openaiShim: {
supportsApiFormatSelection: true,
supportsAuthHeaders: true,
},
},
preset: {
id: 'orcarouter',
description: 'OrcaRouter OpenAI-compatible endpoint',
apiKeyEnvVars: ['ORCAROUTER_API_KEY'],
vendorId: 'openai',
},
catalog: {
source: 'hybrid',
discovery: { kind: 'openai-compatible', mapModel: mapOrcarouterModel },
discoveryCacheTtl: '1d',
discoveryRefreshMode: 'background-if-stale',
allowManualRefresh: true,
models: [
{
id: 'orcarouter-auto',
apiName: 'orcarouter/auto',
label: 'Auto — Smart Routing (via OrcaRouter)',
notes: 'Gateway routes to the best model for each request',
},
{
id: 'orcarouter-gpt-5.5',
apiName: 'openai/gpt-5.5',
label: 'GPT-5.5 (via OrcaRouter)',
modelDescriptorId: 'gpt-5.5',
},
{
id: 'orcarouter-claude-sonnet-4-6',
apiName: 'anthropic/claude-sonnet-4.6',
label: 'Claude Sonnet 4.6 (via OrcaRouter)',
modelDescriptorId: 'claude-sonnet-4-6',
maxOutputTokens: 64_000,
},
{
id: 'orcarouter-gemini-3.5-flash',
apiName: 'google/gemini-3.5-flash',
label: 'Gemini 3.5 Flash (via OrcaRouter)',
modelDescriptorId: 'gemini-3.5-flash',
},
],
},
usage: { supported: false },
})
@@ -50,6 +50,7 @@ import gatewayOllama from '../gateways/ollama.js'
import gatewayOpencodeGo from '../gateways/opencode-go.js'
import gatewayOpencode from '../gateways/opencode.js'
import gatewayOpenrouter from '../gateways/openrouter.js'
import gatewayOrcarouter from '../gateways/orcarouter.js'
import gatewayTogether from '../gateways/together.js'
import gatewayVertex from '../gateways/vertex.js'
import gatewayXiaomiMimoToken from '../gateways/xiaomi-mimo-token.js'
@@ -97,7 +98,7 @@ import modelXai from '../models/xai.js'
import modelXiaomiMimo from '../models/xiaomi-mimo.js'
export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorBankr, vendorDeepseek, vendorFireworks, vendorGeminiVertex, vendorGemini, vendorLongcat, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[]
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApiRoute, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCommandcode, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGmiCloud, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMergeGateway, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[]
export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApiRoute, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCommandcode, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGmiCloud, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMergeGateway, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayOrcarouter, 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[])[]
@@ -493,6 +493,17 @@ export const PROVIDER_PRESET_MANIFEST = [
"OPENROUTER_API_KEY"
]
},
{
"preset": "orcarouter",
"routeKind": "gateway",
"routeId": "orcarouter",
"vendorId": "openai",
"gatewayId": "orcarouter",
"description": "OrcaRouter OpenAI-compatible endpoint",
"apiKeyEnvVars": [
"ORCAROUTER_API_KEY"
]
},
{
"preset": "together",
"routeKind": "gateway",
@@ -666,6 +677,7 @@ export const ORDERED_PROVIDER_PRESETS = [
"opencode-go",
"opencode",
"openrouter",
"orcarouter",
"together",
"venice",
"xai",
+3
View File
@@ -68,6 +68,9 @@ describe('loaded registry validation', () => {
// so there is no concrete model descriptor to reference.
'gitlawb-opengateway:opengateway-auto',
'merge-gateway:merge-gateway-default-routing',
// Virtual model — OrcaRouter's `orcarouter/auto` router resolves the
// serving model server-side, so there is no concrete descriptor.
'orcarouter:orcarouter-auto',
// Cloudflare Workers AI serves provider-specific quantized builds
// (`@cf/...`) with no shared cross-provider model descriptor, the same
// situation as azure-deployment above. See gateways/cloudflare.ts (#1100).
+17
View File
@@ -875,3 +875,20 @@ describe('resolveAgentRunModelRouting: in-process teammate route identity', () =
})
})
})
test.each([undefined, 'parent-orca-key'])(
'spawned Orca override owns its key with ambient %s', ambient => {
const env: NodeJS.ProcessEnv = { ORCAROUTER_API_KEY: ambient }
applyAgentProviderOverrideToEnv({
model: 'openai/gpt-5.5', baseURL: 'https://api.orcarouter.ai/v1',
apiKey: 'configured-agent-key',
}, env)
expect(env.ORCAROUTER_API_KEY).toBe('configured-agent-key')
expect(env.OPENAI_API_KEY).toBe('configured-agent-key')
applyAgentProviderOverrideToEnv({
model: 'custom', baseURL: 'https://custom.example/v1', apiKey: 'custom-key',
}, env)
expect(env.ORCAROUTER_API_KEY).toBeUndefined()
expect(env.OPENAI_API_KEY).toBe('custom-key')
},
)
+4
View File
@@ -57,6 +57,7 @@ const PROVIDER_ENV_VARS_TO_CLEAR_FOR_OVERRIDE = [
'OPENAI_AUTH_SCHEME',
'OPENAI_AUTH_HEADER_VALUE',
'MERGE_GATEWAY_API_KEY',
'ORCAROUTER_API_KEY',
'CMD_API_KEY',
'COMMANDCODE_API_KEY',
'COMMAND_CODE_API_KEY',
@@ -365,6 +366,9 @@ export function applyAgentProviderOverrideToEnv(
env.OPENAI_MODEL = providerOverride.model
env.OPENAI_BASE_URL = providerOverride.baseURL
env.OPENAI_API_KEY = providerOverride.apiKey
if (resolveRouteIdFromBaseUrl(providerOverride.baseURL) === 'orcarouter') {
env.ORCAROUTER_API_KEY = providerOverride.apiKey
}
if (resolveRouteIdFromBaseUrl(providerOverride.baseURL) === 'merge-gateway') {
const key = sanitizeApiKey(providerOverride.apiKey)
if (key) env.MERGE_GATEWAY_API_KEY = key
+22
View File
@@ -2,10 +2,12 @@ import { APIError } from '@anthropic-ai/sdk'
import { afterEach, beforeEach, expect, mock, test } from 'bun:test'
import { getEventListeners } from 'node:events'
import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js'
import { snapshotEnv } from '../../test/envSnapshot.js'
import { asMockFetch } from '../../test/typedMocks.js'
import { _clearRegistryForTesting, ensureIntegrationsLoaded, registerGateway } from '../../integrations/index.ts'
import { applyProviderFlag } from '../../utils/providerFlag.ts'
import { applyProviderProfileToProcessEnv } from '../../utils/providerProfiles.ts'
import { PROFILE_ORCAROUTER_KEY_SHA256 } from '../../utils/providerProfile.js'
import {
__resetInterruptionTraceForTests,
__waitForInterruptionTraceFlushForTests,
@@ -70,6 +72,7 @@ const originalEnv = {
LONGCAT_API_KEY: process.env.LONGCAT_API_KEY,
CLINE_API_KEY: process.env.CLINE_API_KEY,
GMI_API_KEY: process.env.GMI_API_KEY,
ORCAROUTER_API_KEY: process.env.ORCAROUTER_API_KEY,
OPENGATEWAY_API_KEY: process.env.OPENGATEWAY_API_KEY,
OPENGATEWAY_BASE_URL: process.env.OPENGATEWAY_BASE_URL,
OPENCODE_API_KEY: process.env.OPENCODE_API_KEY,
@@ -80,6 +83,7 @@ const originalEnv = {
}
const originalFetch = globalThis.fetch
let restoreOrcarouterCredentialOrigin: () => void
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
@@ -495,6 +499,7 @@ async function captureChatCompletionRequest(
beforeEach(async () => {
await acquireSharedMutationLock('openaiShim.test.ts')
restoreOrcarouterCredentialOrigin = snapshotEnv([PROFILE_ORCAROUTER_KEY_SHA256], { clear: true })
process.env.OPENAI_BASE_URL = 'http://example.test/v1'
delete process.env.OPENAI_API_BASE
process.env.OPENAI_API_KEY = 'test-key'
@@ -532,6 +537,7 @@ beforeEach(async () => {
delete process.env.LONGCAT_API_KEY
delete process.env.CLINE_API_KEY
delete process.env.GMI_API_KEY
delete process.env.ORCAROUTER_API_KEY
delete process.env.OPENGATEWAY_API_KEY
delete process.env.OPENGATEWAY_BASE_URL
delete process.env.OPENCODE_API_KEY
@@ -581,6 +587,7 @@ afterEach(() => {
restoreEnv('LONGCAT_API_KEY', originalEnv.LONGCAT_API_KEY)
restoreEnv('CLINE_API_KEY', originalEnv.CLINE_API_KEY)
restoreEnv('GMI_API_KEY', originalEnv.GMI_API_KEY)
restoreEnv('ORCAROUTER_API_KEY', originalEnv.ORCAROUTER_API_KEY)
restoreEnv('OPENGATEWAY_API_KEY', originalEnv.OPENGATEWAY_API_KEY)
restoreEnv('OPENGATEWAY_BASE_URL', originalEnv.OPENGATEWAY_BASE_URL)
restoreEnv('OPENCODE_API_KEY', originalEnv.OPENCODE_API_KEY)
@@ -592,6 +599,7 @@ afterEach(() => {
_clearRegistryForTesting()
ensureIntegrationsLoaded()
} finally {
restoreOrcarouterCredentialOrigin()
releaseSharedMutationLock()
}
})
@@ -2696,6 +2704,20 @@ test('longcat prefers its dedicated credential over a copied key from another pr
expect(captured.authorization).toBe('Bearer fake-longcat-key')
})
test('an Orca profile sends its selected key instead of the ambient parent key', async () => {
process.env.ORCAROUTER_API_KEY = 'ambient-parent-key'
applyProviderProfileToProcessEnv({
id: 'orca-wire', name: 'Orca wire', provider: 'orcarouter',
baseUrl: 'https://api.orcarouter.ai/v1', model: 'openai/gpt-5.5',
apiKey: 'selected-orca-key',
})
const captured = await captureChatCompletionRequest('openai/gpt-5.5')
expect(captured.url).toBe('https://api.orcarouter.ai/v1/chat/completions')
expect(captured.authorization).toBe('Bearer selected-orca-key')
})
test('longcat provider flag never falls back to an OPENAI_API_KEYS pool', async () => {
process.env.OPENAI_API_KEYS = 'other-provider-secret'
delete process.env.LONGCAT_API_KEY
+202
View File
@@ -0,0 +1,202 @@
import assert from 'node:assert/strict'
import type Anthropic from '@anthropic-ai/sdk'
import { createOpenAIShimClient } from '../../services/api/openaiShim.js'
import { enableConfigs, getGlobalConfig, saveGlobalConfig, type ProviderProfile } from '../../utils/config.js'
import { applyConfigEnvironmentVariables, applySafeConfigEnvironmentVariables } from '../../utils/managedEnv.js'
import { applySavedProfileToCurrentSession, applyStartupEnvFromProfile, buildLaunchEnv, createProfileFile } from '../../utils/providerProfile.js'
import { addProviderProfile, applyActiveProviderProfileFromConfig, getProviderProfiles, setActiveProviderProfile, updateProviderProfile } from '../../utils/providerProfiles.js'
import { getProviderValidationError } from '../../utils/providerValidation.js'
import { quote } from '../../utils/bash/shellQuote.js'
import { buildInheritedEnvVars } from '../../utils/swarm/spawnUtils.js'
const [scenario, provider = 'openai', option] = process.argv.slice(2)
const profile = {
id: 'orca-process', name: 'Orca process', provider,
baseUrl: 'https://api.orcarouter.ai/v1', model: 'openai/gpt-5.5',
apiKey: 'selected-key',
}
enableConfigs()
getGlobalConfig() // Start the normal watcher before either process writes.
let request: { url: string; input: boolean; messages: boolean } | undefined
async function authorization() {
let header: string | null = null
globalThis.fetch = Object.assign(async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
header = new Headers(init?.headers).get('authorization')
const body = JSON.parse(String(init?.body))
request = { url: String(input), input: !!body.input, messages: !!body.messages }
if (request.url.endsWith('/responses')) {
return Response.json({
id: 'reply', object: 'response', status: 'completed', model: profile.model,
output: [{ type: 'message', id: 'msg', role: 'assistant', status: 'completed',
content: [{ type: 'output_text', text: 'done', annotations: [] }] }],
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
})
}
return Response.json({
id: 'reply', model: profile.model,
choices: [{ message: { role: 'assistant', content: 'done' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
})
}, { preconnect: globalThis.fetch.preconnect })
const client = createOpenAIShimClient({}) as Pick<Anthropic, 'messages'>
await client.messages.create({
model: profile.model, messages: [{ role: 'user', content: 'hello' }],
max_tokens: 32, stream: false,
})
return header
}
function activate(selectedProfile: ProviderProfile = profile) {
saveGlobalConfig(current => ({
...current, providerProfiles: [selectedProfile], activeProviderProfileId: selectedProfile.id,
}))
setActiveProviderProfile(selectedProfile.id, { configDir: process.env.OPENCLAUDE_CONFIG_DIR })
}
function finish(value: unknown): never {
console.log(JSON.stringify(value))
process.exit(0)
}
if (scenario === 'pane-child' || scenario === 'pane-keyless-child') {
applySafeConfigEnvironmentVariables()
await applyStartupEnvFromProfile()
const initialAuthorization = await authorization()
if (scenario === 'pane-keyless-child') {
assert(addProviderProfile({ ...profile, name: 'Keyless Orca', apiKey: undefined }))
finish({ initialAuthorization, authorization: await authorization() })
}
finish({ validation: await getProviderValidationError(), authorization: initialAuthorization })
}
if (scenario === 'api-format') {
assert(option === 'responses' || option === 'responses_compat')
saveGlobalConfig(current => ({
...current, providerProfiles: [{ ...profile, apiFormat: option }],
activeProviderProfileId: profile.id,
}))
applySafeConfigEnvironmentVariables()
await applyStartupEnvFromProfile()
finish({ apiFormat: getProviderProfiles()[0]?.apiFormat,
authorization: await authorization(), request })
}
if (scenario === 'legacy-startup' || scenario === 'legacy-session') {
const persisted = createProfileFile('openai', {
OPENAI_BASE_URL: profile.baseUrl, OPENAI_MODEL: profile.model,
OPENAI_API_KEY: profile.apiKey,
})
const error = scenario === 'legacy-startup'
? await applyStartupEnvFromProfile({ persisted })
: await applySavedProfileToCurrentSession({ profileFile: persisted })
assert.equal(error, null)
assert.equal(await authorization(), 'Bearer selected-key')
if (option === 'replacement') process.env.ORCAROUTER_API_KEY = 'replacement-shell-key'
assert(addProviderProfile({ ...profile, name: 'Keyless Orca', apiKey: undefined }))
finish({ authorization: await authorization() })
}
if (scenario === 'shell-switch') {
const first = { ...profile, apiKey: undefined }
const second = { ...first, id: 'second-keyless-orca' }
process.env.ORCAROUTER_API_KEY = 'shell-key'
saveGlobalConfig(current => ({ ...current, providerProfiles: [first, second] }))
const headers: Array<string | null> = []
for (const selected of [first, second, first]) {
setActiveProviderProfile(selected.id, { configDir: process.env.OPENCLAUDE_CONFIG_DIR })
headers.push(await authorization())
assert.equal(getGlobalConfig().providerProfiles?.find(p => p.id === selected.id)?.apiKey, undefined)
}
finish({ headers, validation: await getProviderValidationError() })
}
if (scenario === 'pane') {
if (option === 'shell') {
process.env.ORCAROUTER_API_KEY = 'shell-key'
activate({ ...profile, apiKey: undefined })
} else {
activate()
if (option === 'replacement') process.env.ORCAROUTER_API_KEY = 'replacement-shell-key'
}
// An existing tmux login shell has none of the leader's provider credentials.
const child = Bun.spawnSync(['/bin/sh', '-c',
'env ' + buildInheritedEnvVars() + ' ' +
quote([process.execPath, import.meta.path, option ? 'pane-keyless-child' : 'pane-child', provider]),
], {
env: Object.fromEntries([
'PATH', 'NODE_ENV', 'TMPDIR', 'TMP', 'TEMP', 'CARGO_TARGET_DIR', 'GOCACHE',
'GOMODCACHE', 'NPM_CONFIG_CACHE', 'BUN_INSTALL_CACHE_DIR', 'PIP_CACHE_DIR',
'XDG_CACHE_HOME', 'BUN_RUNTIME_TRANSPILER_CACHE_PATH',
].map(key => [key, process.env[key]])),
timeout: 10_000,
})
assert.equal(child.exitCode, 0, child.stderr.toString())
finish(JSON.parse(child.stdout.toString()))
}
if (scenario === 'host-pre' || scenario === 'host-post') {
if (option !== 'unmanaged') process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST = '1'
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = profile.baseUrl
process.env.OPENAI_MODEL = profile.model
process.env.OPENAI_API_KEY = 'host-key'
process.env.ORCAROUTER_API_KEY = 'host-key'
saveGlobalConfig(current => ({ ...current, env: { ORCAROUTER_API_KEY: 'settings-key' } }))
if (scenario === 'host-pre') applySafeConfigEnvironmentVariables()
else applyConfigEnvironmentVariables()
finish({ authorization: await authorization() })
}
if (scenario === 'edit') {
const edited = updateProviderProfile(profile.id, { ...profile, apiKey: undefined })
assert(edited)
assert.equal(edited.apiKey, undefined)
finish({ savedKeyRemoved: true, authorization: await authorization() })
}
if (scenario === 'remove-inherited') {
activate()
const child = Bun.spawnSync([process.execPath, import.meta.path, 'remove-aligned', provider, option], {
env: process.env, timeout: 10_000,
})
assert.equal(child.exitCode, 0, child.stderr.toString())
finish(JSON.parse(child.stdout.toString()))
}
if (scenario === 'remove-before-startup' || scenario === 'remove-before-launch') {
activate()
if (option === 'generic-removed') delete process.env.OPENAI_API_KEY
if (option === 'replacement') {
process.env.ORCAROUTER_API_KEY = 'replacement-shell-key'
delete process.env.OPENAI_API_KEY
}
let inheritedEnv = { ...process.env }
const editor = Bun.spawnSync([process.execPath, import.meta.path, 'edit', provider], {
env: process.env, timeout: 5_000,
})
assert.equal(editor.exitCode, 0, editor.stderr.toString())
if (scenario === 'remove-before-launch') {
inheritedEnv = await buildLaunchEnv({
profile: 'openai', persisted: null, goal: 'balanced', processEnv: inheritedEnv,
})
}
const child = Bun.spawnSync([process.execPath, import.meta.path, 'pane-child'], {
env: inheritedEnv, timeout: 5_000,
})
assert.equal(child.exitCode, 0, child.stderr.toString())
finish(JSON.parse(child.stdout.toString()))
}
if (scenario === 'remove') activate()
else {
assert.equal(scenario, 'remove-aligned')
applyActiveProviderProfileFromConfig()
}
if (option === 'generic-removed') delete process.env.OPENAI_API_KEY
if (option === 'replacement') {
process.env.ORCAROUTER_API_KEY = 'replacement-shell-key'
delete process.env.OPENAI_API_KEY
}
// Let the subsequent write have a newer timestamp than our write-through cache.
await Bun.sleep(30)
const child = Bun.spawnSync([process.execPath, import.meta.path, 'edit', provider], {
env: process.env, timeout: 5_000,
})
assert.equal(child.exitCode, 0, child.stderr.toString())
const edited = JSON.parse(child.stdout.toString()) as { authorization: string | null }
assert.equal(edited.authorization, option === 'replacement' ? 'Bearer replacement-shell-key' : null)
const deadline = Date.now() + 5_000
while (getGlobalConfig().providerProfiles?.[0]?.apiKey && Date.now() < deadline) {
await Bun.sleep(25)
}
assert.equal(getGlobalConfig().providerProfiles?.[0]?.apiKey, undefined)
applyConfigEnvironmentVariables()
finish({ authorization: await authorization() })
+9
View File
@@ -1309,3 +1309,12 @@ test('clearSessionContextWindowOverride resets state for session isolation', ()
expect(getSessionContextWindowOverride('gpt-4o')).toBeUndefined()
expect(getContextWindowForModel('gpt-4o')).not.toBe(256_000)
})
test.each([[32_000, 32_000], [90_000, 64_000]])(
'Orca Sonnet output request %s uses its routed ceiling', (requested, expected) => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = 'https://api.orcarouter.ai/v1'
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = String(requested)
expect(getMaxOutputTokensForModel('anthropic/claude-sonnet-4.6')).toBe(expected)
},
)
+1
View File
@@ -118,6 +118,7 @@ const ALLOWED_ENV_FILE_KEYS = new Set([
'OLLAMA_API_KEY',
'OLLAMA_BASE_URL',
'OPENROUTER_API_KEY',
'ORCAROUTER_API_KEY',
'OPENAI_API_BASE',
'OPENAI_API_FORMAT',
'OPENAI_API_KEYS',
+1
View File
@@ -51,6 +51,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
'COMMAND_CODE_API_KEY',
'API_ROUTE_API_KEY',
'MERGE_GATEWAY_API_KEY',
'ORCAROUTER_API_KEY',
'OPENAI_API_KEY',
'OPENAI_API_KEYS',
'GEMINI_ACCESS_TOKEN',
+13
View File
@@ -50,6 +50,7 @@ const ENV_KEYS = [
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'GMI_API_KEY',
'ORCAROUTER_API_KEY',
'GMI_MODEL',
'APISMART_API_KEY',
'APISMART_MODEL',
@@ -2086,4 +2087,16 @@ describe('applyProviderFlag - api-route', () => {
})
})
test.each([
['orca-key', undefined], ['independent-openai-key', 'independent-openai-key'],
] as const)('OpenAI startup handles Orca generic alias %s', (generic, expected) => {
process.env.OPENAI_BASE_URL = 'https://api.orcarouter.ai/v1'
process.env.ORCAROUTER_API_KEY = 'orca-key'
process.env.OPENAI_API_KEY = generic
expect(applyProviderFlag('openai', [])).toEqual({})
expect(process.env.OPENAI_BASE_URL).toBe('https://api.openai.com/v1')
if (expected === undefined) expect(process.env.OPENAI_API_KEY).toBeUndefined()
else expect(process.env.OPENAI_API_KEY).toBe(expected)
})
+3
View File
@@ -402,6 +402,9 @@ export function applyProviderFlag(
: process.env.OPENAI_API_KEY !== undefined &&
process.env.OPENAI_API_KEY === process.env.GMI_API_KEY
? 'gmi-cloud'
: process.env.OPENAI_API_KEY !== undefined &&
process.env.OPENAI_API_KEY === process.env.ORCAROUTER_API_KEY
? 'orcarouter'
: process.env.OPENAI_API_KEY !== undefined &&
process.env.OPENAI_API_KEY === process.env.APISMART_API_KEY
? 'apismart'
+35
View File
@@ -3779,3 +3779,38 @@ test('buildStartupEnvFromProfile applies saved API Route proxy profile even when
assert.equal(env.OPENAI_API_KEY, undefined)
assert.equal(env.OPENAI_API_KEYS, undefined)
})
test('Orca relaunch migrates only the selected canonical profile credential', async () => {
const env = await buildLaunchEnv({
profile: 'openai', goal: 'balanced',
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://api.orcarouter.ai/v1',
OPENAI_MODEL: 'openai/gpt-5.5', OPENAI_API_KEY: 'saved-orca-key',
}),
processEnv: { ORCAROUTER_API_KEY: 'ambient-other-account' },
})
assert.equal(resolveRouteCredentialValue({
routeId: 'orcarouter', baseUrl: env.OPENAI_BASE_URL, processEnv: env,
}), 'saved-orca-key')
const ambientOnly = await buildLaunchEnv({
profile: 'openai', persisted: null, goal: 'balanced',
processEnv: {
OPENAI_BASE_URL: 'https://api.orcarouter.ai/v1',
OPENAI_MODEL: 'openai/gpt-5.5', OPENAI_API_KEY: 'unrelated-generic-key',
},
})
assert.equal(ambientOnly.ORCAROUTER_API_KEY, undefined)
})
test('OpenAI profile builder materializes the canonical Orca key only at its route', () => {
const env = buildOpenAIProfileEnv({
goal: 'balanced', baseUrl: 'https://api.orcarouter.ai/v1',
model: 'openai/gpt-5.5', apiKey: 'selected-orca-key', processEnv: {},
})
assert.equal(env?.ORCAROUTER_API_KEY, 'selected-orca-key')
const custom = buildOpenAIProfileEnv({
goal: 'balanced', baseUrl: 'https://custom.example/v1',
model: 'custom', apiKey: 'custom-key', processEnv: {},
})
assert.equal(custom?.ORCAROUTER_API_KEY, undefined)
})
+51
View File
@@ -1,4 +1,5 @@
import { getGeminiVertexProjectId, geminiVertexAdcWillResolveProject } from './geminiAuth.js'
import { createHash } from 'node:crypto'
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import {
@@ -64,8 +65,23 @@ export const DEFAULT_MISTRAL_MODEL = 'mistral-vibe-cli-latest'
export const DEFAULT_STARTUP_PROVIDER_ENV_VAR =
'CLAUDE_CODE_DEFAULT_STARTUP_PROVIDER'
export const PROFILE_ORCAROUTER_KEY_SHA256 =
'CLAUDE_CODE_PROVIDER_PROFILE_ORCAROUTER_KEY_SHA256'
export function orcarouterCredentialFingerprint(value: string): string {
return createHash('sha256').update(sanitizeApiKey(value)?.trim() ?? '').digest('hex')
}
export function isManagedOrcarouterCredential(env: NodeJS.ProcessEnv): boolean {
const key = sanitizeApiKey(env.ORCAROUTER_API_KEY)
// Credential origin is independent of selection markers: legacy profile
// files have no plural profile id, and rebuilding an env can reset selection.
return !!key && env[PROFILE_ORCAROUTER_KEY_SHA256] === orcarouterCredentialFingerprint(key)
}
const PROFILE_ENV_KEYS = [
'CLAUDE_CODE_USE_GEMINI_VERTEX',
PROFILE_ORCAROUTER_KEY_SHA256,
'CLAUDE_CODE_USE_OPENAI',
'CLAUDE_CODE_USE_GITHUB',
'CLAUDE_CODE_USE_GEMINI',
@@ -122,6 +138,7 @@ const PROFILE_ENV_KEYS = [
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'GMI_API_KEY',
'ORCAROUTER_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
'API_ROUTE_API_KEY',
@@ -181,6 +198,7 @@ export type ProviderProfile =
| 'clinepass'
export type ProfileEnv = {
CLAUDE_CODE_PROVIDER_PROFILE_ORCAROUTER_KEY_SHA256?: string
ANTHROPIC_BASE_URL?: string
ANTHROPIC_MODEL?: string
ANTHROPIC_API_KEY?: string
@@ -228,6 +246,7 @@ export type ProfileEnv = {
MIMO_API_KEY?: string
ATLAS_CLOUD_API_KEY?: string
GMI_API_KEY?: string
ORCAROUTER_API_KEY?: string
APISMART_API_KEY?: string
API_ROUTE_API_KEY?: string
CLINE_API_KEY?: string
@@ -1071,6 +1090,9 @@ export function buildOpenAIProfileEnv(options: {
...(resolveRouteIdFromBaseUrl(resolvedBaseUrl) === 'aimlapi' && key
? { AIMLAPI_API_KEY: key }
: {}),
...(resolveRouteIdFromBaseUrl(resolvedBaseUrl) === 'orcarouter' && key
? { ORCAROUTER_API_KEY: key }
: {}),
OPENAI_BASE_URL: resolvedBaseUrl,
OPENAI_MODEL: normalizedModel,
...(options.apiFormat ? { OPENAI_API_FORMAT: options.apiFormat } : {}),
@@ -2306,6 +2328,7 @@ export async function buildLaunchEnv(options: {
for (const dedicatedKey of [
'ATLAS_CLOUD_API_KEY',
'GMI_API_KEY',
'ORCAROUTER_API_KEY',
'APISMART_API_KEY',
'CONCENTRATE_API_KEY',
'LLMTR_API_KEY',
@@ -2347,6 +2370,9 @@ export async function buildLaunchEnv(options: {
if (dedicatedKey === 'APISMART_API_KEY' && effectiveOpenAIRouteId !== 'apismart') {
continue
}
if (dedicatedKey === 'ORCAROUTER_API_KEY' && effectiveOpenAIRouteId !== 'orcarouter') {
continue
}
if (dedicatedKey === 'CONCENTRATE_API_KEY' && effectiveOpenAIRouteId !== 'concentrate') {
continue
}
@@ -2499,18 +2525,35 @@ export async function buildLaunchEnv(options: {
? sanitizeApiKey(persistedOpenAICredential.value)
: undefined)
: undefined
// Recover only the selected canonical profile's own key, including older
// files that predate its dedicated alias. Never promote a shell generic key.
const persistedOrcarouterProfileKey =
dedicatedKey === 'ORCAROUTER_API_KEY' &&
effectiveOpenAIRouteId === 'orcarouter' &&
resolveRouteIdFromBaseUrl(persistedOpenAIBaseUrl) === 'orcarouter'
? sanitizeApiKey(persistedEnv.ORCAROUTER_API_KEY) ||
(persistedOpenAICredential?.kind === 'usable'
? sanitizeApiKey(persistedOpenAICredential.value)
: undefined)
: undefined
const dedicatedValue = withholdAmbientDedicatedKey
? sanitizeApiKey(persistedEnv[dedicatedKey])
: backfillDedicatedFromOpenAI ||
persistedLlmtrProfileKey ||
persistedMergeGatewayProfileKey ||
persistedCommandcodeProfileKey ||
persistedOrcarouterProfileKey ||
sanitizeApiKey(processEnv[dedicatedKey]) ||
sanitizeApiKey(persistedEnv[dedicatedKey]) ||
backfillLegacyApismartProfileKey ||
backfillLegacyConcentrateProfileKey
if (dedicatedValue) {
env[dedicatedKey] = dedicatedValue
if (dedicatedKey === 'ORCAROUTER_API_KEY' &&
(persistedOrcarouterProfileKey ||
(!sanitizeApiKey(processEnv.ORCAROUTER_API_KEY) && persistedEnv.ORCAROUTER_API_KEY))) {
env[PROFILE_ORCAROUTER_KEY_SHA256] = orcarouterCredentialFingerprint(dedicatedValue)
}
}
}
if (effectiveOpenAIRouteId === 'nvidia-nim') {
@@ -2699,8 +2742,16 @@ export function applyProfileEnvToProcessEnv(
targetEnv: NodeJS.ProcessEnv,
nextEnv: NodeJS.ProcessEnv,
): void {
const retainedOrcarouterOrigin =
isManagedOrcarouterCredential(targetEnv) &&
sanitizeApiKey(targetEnv.ORCAROUTER_API_KEY) === sanitizeApiKey(nextEnv.ORCAROUTER_API_KEY)
? targetEnv[PROFILE_ORCAROUTER_KEY_SHA256]
: undefined
clearManagedProfileEnv(targetEnv)
Object.assign(targetEnv, nextEnv)
if (retainedOrcarouterOrigin) {
targetEnv[PROFILE_ORCAROUTER_KEY_SHA256] = retainedOrcarouterOrigin
}
}
type StartupEnvOptions = NonNullable<Parameters<typeof buildStartupEnvFromProfile>[0]>
@@ -0,0 +1,97 @@
import { expect, test } from 'bun:test'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
const fixture = fileURLToPath(new URL('../test/fixtures/orcarouterCredentials.fixture.ts', import.meta.url))
const childEnvKeys = [
'PATH', 'TMPDIR', 'TMP', 'TEMP', 'CARGO_TARGET_DIR', 'GOCACHE', 'GOMODCACHE',
'NPM_CONFIG_CACHE', 'BUN_INSTALL_CACHE_DIR', 'PIP_CACHE_DIR', 'XDG_CACHE_HOME',
'BUN_RUNTIME_TRANSPILER_CACHE_PATH',
]
function run(scenario: string, provider = 'openai', option = '') {
const configDir = mkdtempSync(join(tmpdir(), 'orca-credentials-'))
try {
const child = Bun.spawnSync([process.execPath, fixture, scenario, provider, option], {
env: {
...Object.fromEntries(childEnvKeys.map(key => [key, process.env[key]])),
OPENCLAUDE_CONFIG_DIR: configDir, NODE_ENV: 'development',
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
},
// Real config polling and a second executable need more than the default
// test deadline: child deadline 15s, test deadline 20s allows teardown.
timeout: 15_000,
})
expect(child.exitCode, child.stderr.toString()).toBe(0)
return JSON.parse(child.stdout.toString().trim()) as Record<string, unknown>
} finally {
rmSync(configDir, { recursive: true, force: true })
}
}
test.skipIf(process.platform === 'win32')('Orca pane inheritance authenticates after real child startup', () => {
const result = run('pane')
expect(result.validation).toBeNull()
expect(result.authorization).toBe('Bearer selected-key')
}, 20_000)
for (const provider of ['openai', 'orcarouter']) {
for (const [option, initialAuthorization, authorization] of [
['saved', 'Bearer selected-key', null],
['shell', 'Bearer shell-key', 'Bearer shell-key'],
['replacement', 'Bearer replacement-shell-key', 'Bearer replacement-shell-key'],
] as const) {
test.skipIf(process.platform === 'win32')(`${provider} Orca pane keyless switch respects ${option} credential ownership`, () => {
expect(run('pane', provider, option)).toEqual({ initialAuthorization, authorization })
}, 20_000)
}
}
for (const phase of ['host-pre', 'host-post']) {
test.each([true, false])(`Orca ${phase} settings merge preserves credential ownership (host managed: %s)`, managed => {
expect(run(phase, 'openai', managed ? '' : 'unmanaged').authorization)
.toBe(managed ? 'Bearer host-key' : 'Bearer settings-key')
}, 20_000)
}
for (const provider of ['openai', 'orcarouter']) {
for (const scenario of ['legacy-startup', 'legacy-session']) {
test.each(['', 'replacement'])(`${provider} Orca keyless selection after ${scenario} rejects saved credentials (%s)`, option => {
expect(run(scenario, provider, option).authorization)
.toBe(option === 'replacement' ? 'Bearer replacement-shell-key' : null)
}, 20_000)
}
test.each(['', 'generic-removed', 'replacement'])(
`${provider} Orca launcher preserves removed-key ownership (%s)`, option => {
expect(run('remove-before-launch', provider, option).authorization)
.toBe(option === 'replacement' ? 'Bearer replacement-shell-key' : null)
}, 20_000)
test(`${provider} Orca keyless profile switches retain genuine shell authentication`, () => {
const result = run('shell-switch', provider)
expect(result.headers).toEqual(['Bearer shell-key', 'Bearer shell-key', 'Bearer shell-key'])
expect(result.validation).toBeNull()
}, 20_000)
test.each(['', 'generic-removed', 'replacement'])(
`${provider} Orca startup rejects an inherited key removed before startup (%s)`, option => {
expect(run('remove-before-startup', provider, option).authorization)
.toBe(option === 'replacement' ? 'Bearer replacement-shell-key' : null)
}, 20_000)
test(`${provider} Orca initializes ownership when startup inherits an aligned profile`, () => {
expect(run('remove-inherited', provider, 'generic-removed').authorization).toBeNull()
}, 20_000)
test.each(['', 'generic-removed', 'replacement'])(
`${provider} Orca reconciles another process's key removal (%s)`, option => {
expect(run('remove', provider, option).authorization)
.toBe(option === 'replacement' ? 'Bearer replacement-shell-key' : null)
}, 20_000)
}
for (const provider of ['openai', 'custom']) {
test.each(['responses', 'responses_compat'])(`${provider} Orca startup preserves explicit API format %s`, apiFormat => {
const result = run('api-format', provider, apiFormat)
expect(result.apiFormat).toBe(apiFormat)
expect(result.authorization).toBe('Bearer selected-key')
expect(result.request).toEqual({
url: 'https://api.orcarouter.ai/v1/responses', input: true, messages: false,
})
}, 20_000)
}
+85
View File
@@ -25,6 +25,7 @@ const originalFetch = globalThis.fetch
const RESTORED_KEYS = [
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED',
'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID',
'CLAUDE_CODE_PROVIDER_PROFILE_ORCAROUTER_KEY_SHA256',
'CLAUDE_CODE_PROVIDER_ROUTE_ID',
'CLAUDE_CONFIG_DIR',
'OPENCLAUDE_CONFIG_DIR',
@@ -85,6 +86,7 @@ const RESTORED_KEYS = [
'MIMO_API_KEY',
'ATLAS_CLOUD_API_KEY',
'GMI_API_KEY',
'ORCAROUTER_API_KEY',
'APISMART_API_KEY',
'APISMART_MODEL',
'API_ROUTE_API_KEY',
@@ -6475,3 +6477,86 @@ test('DEFAULT_MISTRAL_MODEL matches the mistral gateway defaultModel', async ()
expect(mistralGateway.defaultModel).toBeDefined()
expect(DEFAULT_MISTRAL_MODEL).toBe(mistralGateway.defaultModel!)
})
test('keyless canonical Orca profile retains the dedicated shell credential', async () => {
const { applyProviderProfileToProcessEnv } = await importFreshProviderProfileModules()
process.env.ORCAROUTER_API_KEY = 'ambient-orca-key'
const saved = buildProfile({
provider: 'openai', baseUrl: 'https://api.orcarouter.ai/v1',
model: 'openai/gpt-5.5', apiKey: undefined,
})
applyProviderProfileToProcessEnv(saved)
expect(resolveRouteCredentialValue({
routeId: 'orcarouter', baseUrl: saved.baseUrl,
})).toBe('ambient-orca-key')
applyProviderProfileToProcessEnv(saved)
expect(process.env.ORCAROUTER_API_KEY).toBe('ambient-orca-key')
applyProviderProfileToProcessEnv({ ...saved, baseUrl: 'https://custom.example/v1' })
expect(process.env.ORCAROUTER_API_KEY).toBeUndefined()
})
test.each([false, true])('a keyless Orca selection rejects another profile key (generic removed: %s)', async removeGeneric => {
const { setActiveProviderProfile } = await importFreshProviderProfileModules()
const keyed = buildProfile({ id: 'keyed-orca', provider: 'orcarouter',
baseUrl: 'https://api.orcarouter.ai/v1', model: 'openai/gpt-5.5', apiKey: 'account-a-key' })
const keyless = { ...keyed, id: 'keyless-orca', apiKey: undefined }
saveMockGlobalConfig(current => ({ ...current, providerProfiles: [keyed, keyless] }))
setActiveProviderProfile(keyed.id, { configDir: testConfigDir! })
if (removeGeneric) delete process.env.OPENAI_API_KEY
setActiveProviderProfile(keyless.id, { configDir: testConfigDir! })
expect(process.env.ORCAROUTER_API_KEY).toBeUndefined()
expect(resolveRouteCredentialValue({ routeId: 'orcarouter', baseUrl: keyless.baseUrl }))
.toBeUndefined()
const persisted = JSON.parse(readFileSync(join(testConfigDir!, '.openclaude-profile.json'), 'utf8'))
expect(persisted.env.ORCAROUTER_API_KEY).toBeUndefined()
})
test.each([
['removed-account-key', undefined],
['replacement-shell-key', 'replacement-shell-key'],
] as const)('removing the active Orca profile key handles current key %s', async (currentKey, expected) => {
const { setActiveProviderProfile, updateProviderProfile } = await importFreshProviderProfileModules()
const keyed = buildProfile({ provider: 'orcarouter', baseUrl: 'https://api.orcarouter.ai/v1',
model: 'openai/gpt-5.5', apiKey: 'removed-account-key' })
saveMockGlobalConfig(current => ({ ...current, providerProfiles: [keyed] }))
setActiveProviderProfile(keyed.id, { configDir: testConfigDir! })
process.env.ORCAROUTER_API_KEY = currentKey
delete process.env.OPENAI_API_KEY
const edited = updateProviderProfile(keyed.id, { name: keyed.name, provider: keyed.provider,
baseUrl: keyed.baseUrl, model: keyed.model, apiKey: undefined })
expect(edited?.apiKey).toBeUndefined()
if (expected === undefined) expect(process.env.ORCAROUTER_API_KEY).toBeUndefined()
else expect(process.env.ORCAROUTER_API_KEY).toBe(expected)
})
for (const provider of ['orcarouter', 'openai']) {
test(`${provider} Orca profile owns its credential through apply, persist and relaunch`, async () => {
const { setActiveProviderProfile, applyActiveProviderProfileFromConfig } =
await importFreshProviderProfileModules()
const saved = buildProfile({
provider, apiKey: 'selected-orca-key',
baseUrl: 'https://api.orcarouter.ai/v1', model: 'openai/gpt-5.5',
})
saveMockGlobalConfig(current => ({ ...current, providerProfiles: [saved] }))
process.env.ORCAROUTER_API_KEY = 'ambient-parent-key'
setActiveProviderProfile(saved.id, { configDir: testConfigDir! })
expect(resolveRouteCredentialValue({
routeId: 'orcarouter', baseUrl: saved.baseUrl,
})).toBe('selected-orca-key')
process.env.ORCAROUTER_API_KEY = 'drifted-key'
expect(applyActiveProviderProfileFromConfig()?.id).toBe(saved.id)
expect(process.env.ORCAROUTER_API_KEY).toBe('selected-orca-key')
const persisted = JSON.parse(readFileSync(
join(testConfigDir!, '.openclaude-profile.json'), 'utf8',
))
expect(persisted.env.ORCAROUTER_API_KEY).toBe('selected-orca-key')
const { buildStartupEnvFromProfile } = await import('./providerProfile.js')
const env = await buildStartupEnvFromProfile({
persisted, processEnv: { ORCAROUTER_API_KEY: 'other-ambient-key' },
})
expect(resolveRouteCredentialValue({
routeId: 'orcarouter', baseUrl: saved.baseUrl, processEnv: env,
})).toBe('selected-orca-key')
})
}
+43
View File
@@ -38,6 +38,9 @@ import {
buildApismartProfileEnv,
buildVertexProfileEnv,
clearManagedProfileEnv,
isManagedOrcarouterCredential,
orcarouterCredentialFingerprint,
PROFILE_ORCAROUTER_KEY_SHA256,
deleteProfileFile,
type ProfileFileLocation,
type ProfileEnv,
@@ -117,6 +120,21 @@ export type ProviderPresetDefaults = Omit<ProviderProfileInput, 'provider'> & {
const PROFILE_ENV_APPLIED_FLAG = 'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED'
const PROFILE_ENV_APPLIED_ID = 'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID'
function rememberManagedOrcarouterCredential(
profile: ProviderProfile,
processEnv: NodeJS.ProcessEnv = process.env,
): void {
const value = sanitizeApiKey(profile.apiKey)?.trim()
if (value && resolveRouteIdFromBaseUrl(profile.baseUrl) === 'orcarouter' &&
processEnv[PROFILE_ENV_APPLIED_FLAG] === '1' &&
trimOrUndefined(processEnv[PROFILE_ENV_APPLIED_ID]) === profile.id &&
sanitizeApiKey(processEnv.ORCAROUTER_API_KEY)?.trim() === value) {
// Inherit provenance with the applied markers: saved config can change
// before a child starts, so it cannot reconstruct ownership from config.
processEnv[PROFILE_ORCAROUTER_KEY_SHA256] = orcarouterCredentialFingerprint(value)
}
}
type ProfileCompatibilityMode =
| 'anthropic'
| 'gemini'
@@ -864,6 +882,9 @@ function getHostDedicatedCredentialEnv(
route?: { routeId?: string; vendorId?: string },
): Record<string, string> {
const env: Record<string, string> = {}
if (apiKey && resolveRouteIdFromBaseUrl(baseUrl) === 'orcarouter') {
env.ORCAROUTER_API_KEY = apiKey
}
if (route?.routeId === 'xai' || isXaiBaseUrl(baseUrl)) {
env.XAI_API_KEY = apiKey
}
@@ -901,6 +922,11 @@ function hostDedicatedCredentialsAligned(
if (!includeApiKey) {
return true
}
if (!profile.apiKey &&
resolveRouteIdFromBaseUrl(profile.baseUrl) === 'orcarouter' &&
isManagedOrcarouterCredential(processEnv)) {
return false
}
const dedicated = getHostDedicatedCredentialEnv(
profile.baseUrl,
profile.apiKey ?? '',
@@ -1270,6 +1296,17 @@ export function applyProviderProfileToProcessEnv(
OPENAI_BASE_URL: normalizedProfileBaseUrl,
OPENAI_MODEL: primaryModel,
}
// Keyless canonical profiles already accept the dedicated shell key.
// Reuse only actual shell input, not a key installed by this or another
// profile, including when another CLI removed this profile's saved key.
// Changing profile IDs does not change the origin of genuine shell input.
if (!profile.apiKey &&
resolveRouteIdFromBaseUrl(normalizedProfileBaseUrl) === 'orcarouter') {
const ambientKey = sanitizeApiKey(process.env.ORCAROUTER_API_KEY)
if (ambientKey && !isManagedOrcarouterCredential(process.env)) {
openAIProfileEnv.ORCAROUTER_API_KEY = ambientKey
}
}
const isAimlapiProfile =
profile.provider === 'aimlapi' ||
route.routeId === 'aimlapi' ||
@@ -1543,6 +1580,7 @@ export function applyProviderProfileToProcessEnv(
Object.assign(process.env, nextEnv)
process.env[PROFILE_ENV_APPLIED_FLAG] = '1'
process.env[PROFILE_ENV_APPLIED_ID] = profile.id
rememberManagedOrcarouterCredential(profile)
}
export function applyActiveProviderProfileFromConfig(
@@ -1580,6 +1618,10 @@ export function applyActiveProviderProfileFromConfig(
return undefined
}
// Bootstrap older inherited environments while the matching saved keyed
// record is available; new environments already carry their fingerprint.
rememberManagedOrcarouterCredential(activeProfile, processEnv)
const isCurrentEnvProfileManaged =
processEnv[PROFILE_ENV_APPLIED_FLAG] === '1' &&
trimOrUndefined(processEnv[PROFILE_ENV_APPLIED_ID]) === activeProfile.id
@@ -1696,6 +1738,7 @@ export function updateProviderProfile(
}
wasUpdated = true
rememberManagedOrcarouterCredential(currentProfiles[profileIndex]!)
const nextProfiles = [...currentProfiles]
nextProfiles[profileIndex] = updatedProfile
+3
View File
@@ -115,6 +115,9 @@ const TEAMMATE_ENV_VARS = [
'COMMANDCODE_API_KEY',
'COMMAND_CODE_API_KEY',
'GMI_API_KEY',
'ORCAROUTER_API_KEY',
// Preserve saved-key origin across panes for subsequent keyless selection.
'CLAUDE_CODE_PROVIDER_PROFILE_ORCAROUTER_KEY_SHA256',
'OPENAI_BASE_URL',
'OPENAI_API_BASE',
'OPENAI_MODEL',
+8
View File
@@ -156,6 +156,14 @@ export const providers: Provider[] = [
envVars: ['MERGE_GATEWAY_API_KEY'],
notes: 'Multi-provider model router with Chat Completions, Responses, authenticated catalog discovery, and routing policies.',
},
{
id: 'orcarouter',
name: 'OrcaRouter',
group: 'gateways',
setup: '/provider or ORCAROUTER_API_KEY with --provider orcarouter',
envVars: ['ORCAROUTER_API_KEY'],
notes: 'OpenAI-compatible model routing gateway with named auto routing. Existing environment setups must replace OPENAI_API_KEY with ORCAROUTER_API_KEY, even when a custom authentication header supplies the request credential. Custom headers remain supported.',
},
{
id: 'near-ai',
name: 'NEAR AI',