mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-06 12:50:54 -04:00
fix(provider): match MiniMax and Venice base URLs by hostname, not substring (#2099)
* fix(provider): match MiniMax and Venice base URLs by hostname, not substring
The credential-mirroring chains route xAI, Cloudflare and Xiaomi through their
exact-hostname predicates (isXaiBaseUrl, isCloudflareBaseUrl,
isXiaomiMimoBaseUrl), but the MiniMax and Venice branches were left on raw
baseUrl.toLowerCase().includes('minimax') / includes('api.venice.ai') even
though isMiniMaxBaseUrl / isVeniceBaseUrl already exist. A proxy or lookalike
host (minimax-proxy.example.com, api.venice.ai.evil.com) therefore mirrors the
profile key into MINIMAX_API_KEY / VENICE_API_KEY; for MiniMax, providerAutoDetect
then reroutes it to the real api.minimax.io the user never configured. Route all
four sites through the hostname predicates, consistent with the neighbouring
providers.
* fix(provider): centralize dedicated-key mirroring across the profile lifecycle
The host-detected dedicated credential mapping (xAI, MiniMax, Venice,
MiMo) was reconstructed independently in three places and had drifted:
- The startup env builder's strict-env branch returned before any of
these mirrors, so a keyed generic OpenAI-compatible profile on a
canonical Venice/MiniMax host persisted only OPENAI_API_KEY and
relaunched without its dedicated credential; the fallback branch had
no MiniMax mirror at all and matched MiMo by substring.
- isProcessEnvAlignedWithProfile never checked MINIMAX_API_KEY, so once
that key drifted after activation the env was still considered aligned
and applyActiveProviderProfileFromConfig skipped reapplying — leaving
MiniMax's dedicated-credential transport misrouted.
Introduce getHostDedicatedCredentialEnv as the single source of truth
for the exact-host (and, where available, route) credential mapping, and
route live application, both startup-persistence branches, and the
alignment check through it. Lookalike hosts still receive no dedicated
key.
* fix(provider): carry MiniMax dedicated key through launch reconstruction
buildLaunchEnv rebuilt the startup environment from a fixed dedicated-key
carry-over list that omitted MINIMAX_API_KEY, so a clean restart of a keyed
MiniMax profile retained the generic OpenAI variables but dropped the
dedicated credential. The MiniMax client only enters its native setup while
MINIMAX_API_KEY is present, so the persisted profile did not deliver the
intended restart behavior.
Add MINIMAX_API_KEY to the carry-over contract alongside the other
native-client vendor keys (MIMO/VENICE), preserving the same
shell-over-persisted precedence. The client's own hostname/route detection
gates activation, so an ambient key stays inert in an unrelated session.
Regressions reconstruct the launch environment from a saved profile with an
empty shell and assert both the surviving credential and the selected
env-only route (via resolveEnvOnlyProviderRouteId), not just the persisted
file: canonical MiniMax carries and reselects, a live shell key overrides
the persisted one, and a generic profile never synthesizes the credential.
* fix(provider): preserve keyless credential reconciliation
* fix(provider): scope MiniMax launch credentials to the active route
* fix(provider): admit saved Venice and MiMo keys only on their routes
* test(provider): document saved dedicated-key precedence
* fix(provider): restore test globals and scope profile docs
* fix(provider): keep explicit MiniMax route credentials across reconcile
Reconcile reapply rebuilt a generic OpenAI profile from the hostname only, so a launch that had already kept CLAUDE_CODE_PROVIDER_ROUTE_ID=minimax on a proxy lost MINIMAX_API_KEY when another env field drifted. Reapply now reuses that same-profile route instead of URL substring matching.
* fix(provider): preserve saved credentials in pane teammates
---------
Co-authored-by: jatmn <the@jat.mn>
This commit is contained in:
@@ -837,6 +837,18 @@ bun run dev:atomic-chat
|
||||
|
||||
If no profile exists yet, `dev:profile` uses the same goal-aware defaults when picking the initial model.
|
||||
|
||||
Profiles activated through `/provider` for canonical MiniMax, Venice, and MiMo
|
||||
endpoints retain a provider-specific key, including profiles configured as generic
|
||||
OpenAI-compatible providers. On the same route, that saved key takes precedence over an ambient
|
||||
`OPENAI_API_KEY` or `OPENAI_API_KEYS` pool. To override it for a launch, set
|
||||
`MINIMAX_API_KEY`, `VENICE_API_KEY`, or `MIMO_API_KEY` in your shell; the live
|
||||
provider-specific value takes precedence over the saved value. This applies to
|
||||
both MiMo API and token-plan routes.
|
||||
|
||||
Launch reconstruction retains these dedicated keys only for their matching
|
||||
routes. A shell override to another provider's URL does not carry the saved
|
||||
source provider's dedicated key into the new launch.
|
||||
|
||||
### Provider Profile Model Picker Mode
|
||||
|
||||
When a saved provider profile is active, `/model` can either show the provider's
|
||||
|
||||
@@ -8,6 +8,12 @@ import {
|
||||
} from '../../integrations/index.js'
|
||||
import { publicBuildVersion } from '../../utils/version.js'
|
||||
import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js'
|
||||
import { snapshotEnv } from '../../test/envSnapshot.js'
|
||||
import {
|
||||
buildLaunchEnv,
|
||||
buildStartupEnvFromProfile,
|
||||
createProfileFile,
|
||||
} from '../../utils/providerProfile.js'
|
||||
|
||||
// bun:test keeps mock.module() registrations process-global across test files.
|
||||
// Load and re-register the real module before importing the client so a prior
|
||||
@@ -833,6 +839,77 @@ for (const baseEnv of ['OPENAI_BASE_URL', 'OPENAI_API_BASE']) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of ['persisted', 'ambient'] as const) {
|
||||
test(`profile launch uses the Venice credential despite a ${source} MiniMax alias`, async () => {
|
||||
clearEnvForMiniMaxOnlyTest()
|
||||
const env = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
persisted: createProfileFile('openai', {
|
||||
OPENAI_BASE_URL: 'https://api.minimax.io/v1',
|
||||
OPENAI_MODEL: 'MiniMax-M3',
|
||||
OPENAI_API_KEY: 'saved-minimax-key',
|
||||
MINIMAX_API_KEY: 'saved-minimax-key',
|
||||
}),
|
||||
goal: 'coding',
|
||||
processEnv: {
|
||||
OPENAI_BASE_URL: 'https://api.venice.ai/api/v1',
|
||||
OPENAI_MODEL: 'venice-uncensored',
|
||||
VENICE_API_KEY: 'selected-venice-key',
|
||||
...(source === 'ambient' ? {
|
||||
OPENAI_API_KEY: 'ambient-minimax-key',
|
||||
MINIMAX_API_KEY: 'ambient-minimax-key',
|
||||
} : {}),
|
||||
},
|
||||
})
|
||||
const restore = snapshotEnv(Object.keys(env))
|
||||
try {
|
||||
Object.assign(process.env, env)
|
||||
let capturedUrl: string | undefined
|
||||
let capturedHeaders: Headers | undefined
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
capturedUrl = String(input)
|
||||
capturedHeaders = new Headers(init?.headers)
|
||||
return new Response(JSON.stringify({
|
||||
id: 'chat-venice', object: 'chat.completion', model: 'venice-uncensored',
|
||||
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
|
||||
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
||||
}), { headers: { 'Content-Type': 'application/json' } })
|
||||
}) as FetchType
|
||||
const client = await getAnthropicClient({ maxRetries: 0, model: 'venice-uncensored' })
|
||||
await client.beta.messages.create({
|
||||
model: 'venice-uncensored', max_tokens: 16,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
})
|
||||
expect(capturedUrl).toBe('https://api.venice.ai/api/v1/chat/completions')
|
||||
expect(capturedHeaders?.get('authorization')).toBe('Bearer selected-venice-key')
|
||||
expect(env.MINIMAX_API_KEY).toBeUndefined()
|
||||
} finally {
|
||||
restore()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('saved OpenAI startup keeps its provider label despite an ambient MiniMax key', async () => {
|
||||
clearEnvForMiniMaxOnlyTest()
|
||||
const env = await buildStartupEnvFromProfile({
|
||||
persisted: createProfileFile('openai', {
|
||||
OPENAI_BASE_URL: 'https://api.openai.com/v1',
|
||||
OPENAI_MODEL: 'gpt-4o',
|
||||
OPENAI_API_KEY: 'saved-openai-key',
|
||||
}),
|
||||
processEnv: { MINIMAX_API_KEY: 'ambient-minimax-key' },
|
||||
})
|
||||
const restore = snapshotEnv(Object.keys(env))
|
||||
try {
|
||||
Object.assign(process.env, env)
|
||||
const { detectProvider } = await import('../../components/StartupScreen.js')
|
||||
expect(detectProvider().name).toBe('OpenAI')
|
||||
expect(env.MINIMAX_API_KEY).toBeUndefined()
|
||||
} finally {
|
||||
restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('selected Gemini ignores dormant native MiniMax transport settings', async () => {
|
||||
clearEnvForMiniMaxOnlyTest()
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
|
||||
@@ -7,6 +7,7 @@ import test, { afterEach, beforeEach } from 'node:test'
|
||||
import { acquireEnvMutex, releaseEnvMutex } from '../entrypoints/sdk/shared.js'
|
||||
import {
|
||||
resolveActiveRouteIdFromEnv,
|
||||
resolveEnvOnlyProviderRouteId,
|
||||
resolveRouteCredentialValue,
|
||||
} from '../integrations/routeMetadata.js'
|
||||
import { DEFAULT_CODEX_BASE_URL } from '../services/api/providerConfig.js'
|
||||
@@ -297,6 +298,97 @@ test('openai launch preserves a persisted GMI Cloud key and prefers a live one',
|
||||
assert.equal(rotated.GMI_API_KEY, 'gmi-rotated-key')
|
||||
})
|
||||
|
||||
for (const { baseUrl, routeId } of [
|
||||
{ baseUrl: 'https://api.minimax.io/v1' },
|
||||
{ baseUrl: 'https://api.minimaxi.com/v1' },
|
||||
{ baseUrl: 'https://minimax-proxy.example.com/v1', routeId: 'minimax' },
|
||||
]) {
|
||||
test(`openai launch preserves a persisted MiniMax credential and route at ${baseUrl}`, async () => {
|
||||
// Keep the dedicated key for canonical endpoints and an explicitly saved
|
||||
// route identity without changing the OpenAI-compatible endpoint/protocol.
|
||||
const env = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
persisted: profile('openai', {
|
||||
OPENAI_BASE_URL: baseUrl,
|
||||
OPENAI_MODEL: 'MiniMax-M2.5',
|
||||
...(routeId ? { CLAUDE_CODE_PROVIDER_ROUTE_ID: routeId } : {}),
|
||||
OPENAI_API_KEY: 'minimax-secret-key',
|
||||
MINIMAX_API_KEY: 'minimax-secret-key',
|
||||
}),
|
||||
goal: 'coding',
|
||||
processEnv: {},
|
||||
})
|
||||
|
||||
assert.equal(env.OPENAI_BASE_URL, baseUrl)
|
||||
assert.equal(env.MINIMAX_API_KEY, 'minimax-secret-key')
|
||||
// Provider identity and wire protocol are separate contracts.
|
||||
assert.equal(env.CLAUDE_CODE_USE_OPENAI, '1')
|
||||
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'minimax')
|
||||
})
|
||||
}
|
||||
|
||||
for (const [routeId, key] of [
|
||||
['venice', 'VENICE_API_KEY'],
|
||||
['xiaomi-mimo', 'MIMO_API_KEY'],
|
||||
['xiaomi-mimo-token', 'MIMO_API_KEY'],
|
||||
] as const) {
|
||||
test(`openai launch retains ${routeId} credentials for an explicitly saved route`, async () => {
|
||||
const baseUrl = 'https://configured-proxy.example/v1'
|
||||
const env = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
persisted: profile('openai', {
|
||||
OPENAI_BASE_URL: baseUrl,
|
||||
OPENAI_MODEL: 'configured-model',
|
||||
CLAUDE_CODE_PROVIDER_ROUTE_ID: routeId,
|
||||
OPENAI_API_KEY: 'saved-key',
|
||||
[key]: 'saved-key',
|
||||
}),
|
||||
goal: 'coding',
|
||||
processEnv: {},
|
||||
})
|
||||
assert.equal(env.OPENAI_BASE_URL, baseUrl)
|
||||
assert.equal(env.CLAUDE_CODE_USE_OPENAI, '1')
|
||||
assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, routeId)
|
||||
assert.equal(env[key], 'saved-key')
|
||||
})
|
||||
}
|
||||
|
||||
test('openai launch prefers a live MiniMax key over the persisted one', async () => {
|
||||
const env = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
persisted: profile('openai', {
|
||||
OPENAI_BASE_URL: 'https://api.minimax.io/v1',
|
||||
OPENAI_MODEL: 'MiniMax-M2.5',
|
||||
OPENAI_API_KEY: 'persisted-key',
|
||||
MINIMAX_API_KEY: 'persisted-key',
|
||||
}),
|
||||
goal: 'coding',
|
||||
processEnv: { MINIMAX_API_KEY: 'live-shell-key' },
|
||||
})
|
||||
|
||||
assert.equal(env.MINIMAX_API_KEY, 'live-shell-key')
|
||||
assert.equal(resolveEnvOnlyProviderRouteId(env), 'minimax')
|
||||
})
|
||||
|
||||
test('openai launch does not synthesize a MiniMax credential for a generic profile', async () => {
|
||||
// A generic OpenAI-compatible profile on an unrelated host never persisted a
|
||||
// MINIMAX_API_KEY (the host guard on the persistence side withholds it), so a
|
||||
// clean relaunch must not conjure one or route to MiniMax.
|
||||
const env = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
persisted: profile('openai', {
|
||||
OPENAI_BASE_URL: 'https://api.example.com/v1',
|
||||
OPENAI_MODEL: 'gpt-4o',
|
||||
OPENAI_API_KEY: 'generic-key',
|
||||
}),
|
||||
goal: 'coding',
|
||||
processEnv: {},
|
||||
})
|
||||
|
||||
assert.equal(env.MINIMAX_API_KEY, undefined)
|
||||
assert.notEqual(resolveEnvOnlyProviderRouteId(env), 'minimax')
|
||||
})
|
||||
|
||||
test('openai launch preserves persisted ApiSmart dedicated credentials across restart', async () => {
|
||||
const env = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
|
||||
@@ -2315,6 +2315,7 @@ export async function buildLaunchEnv(options: {
|
||||
'FIREWORKS_API_KEY',
|
||||
'LONGCAT_API_KEY',
|
||||
'AIMLAPI_API_KEY',
|
||||
'MINIMAX_API_KEY',
|
||||
'MIMO_API_KEY',
|
||||
'NVIDIA_API_KEY',
|
||||
'VENICE_API_KEY',
|
||||
@@ -2326,6 +2327,21 @@ export async function buildLaunchEnv(options: {
|
||||
if (dedicatedKey === 'AIMLAPI_API_KEY' && effectiveOpenAIRouteId !== 'aimlapi') {
|
||||
continue
|
||||
}
|
||||
// These aliases also affect copied-key precedence and provider display.
|
||||
// Admit them only for the selected route, including after a URL override.
|
||||
if (dedicatedKey === 'MINIMAX_API_KEY' && effectiveOpenAIRouteId !== 'minimax') {
|
||||
continue
|
||||
}
|
||||
if (dedicatedKey === 'VENICE_API_KEY' && effectiveOpenAIRouteId !== 'venice') {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
dedicatedKey === 'MIMO_API_KEY' &&
|
||||
effectiveOpenAIRouteId !== 'xiaomi-mimo' &&
|
||||
effectiveOpenAIRouteId !== 'xiaomi-mimo-token'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (dedicatedKey === 'APISMART_API_KEY' && effectiveOpenAIRouteId !== 'apismart') {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
import { acquireEnvMutex, releaseEnvMutex } from '../entrypoints/sdk/shared.js'
|
||||
import { resolveRouteCredentialValue } from '../integrations/routeMetadata.js'
|
||||
import { snapshotEnv } from '../test/envSnapshot.js'
|
||||
import type { ProviderProfile } from './config.js'
|
||||
|
||||
const nonce = `${Date.now()}-${Math.random()}`
|
||||
@@ -2417,6 +2418,44 @@ describe('applyProviderProfileToProcessEnv', () => {
|
||||
expect(getFreshAPIProvider()).not.toBe('xai')
|
||||
})
|
||||
|
||||
test('does not mirror MINIMAX_API_KEY for a lookalike host containing "minimax"', async () => {
|
||||
const { applyProviderProfileToProcessEnv } =
|
||||
await importFreshProviderProfileModules()
|
||||
|
||||
// A bare includes('minimax') matched any URL carrying that word — a proxy
|
||||
// host or a lookalike like `api.minimax.io.evil.com` — and mirrored the key
|
||||
// into MINIMAX_API_KEY, which providerAutoDetect then redirects to the real
|
||||
// api.minimax.io. Match by hostname (isMiniMaxBaseUrl), not substring.
|
||||
applyProviderProfileToProcessEnv(
|
||||
buildProfile({
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://minimax-proxy.example.com/v1',
|
||||
model: 'some-model',
|
||||
apiKey: 'not-a-minimax-key',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(process.env.MINIMAX_API_KEY).toBeUndefined()
|
||||
expect(process.env.OPENAI_API_KEY).toBe('not-a-minimax-key')
|
||||
})
|
||||
|
||||
test('does not mirror VENICE_API_KEY for a lookalike host containing "api.venice.ai"', async () => {
|
||||
const { applyProviderProfileToProcessEnv } =
|
||||
await importFreshProviderProfileModules()
|
||||
|
||||
applyProviderProfileToProcessEnv(
|
||||
buildProfile({
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://api.venice.ai.evil.com/v1',
|
||||
model: 'some-model',
|
||||
apiKey: 'not-a-venice-key',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(process.env.VENICE_API_KEY).toBeUndefined()
|
||||
expect(process.env.OPENAI_API_KEY).toBe('not-a-venice-key')
|
||||
})
|
||||
|
||||
test('openai-compatible profile applies maxContextLength to every configured model', async () => {
|
||||
const { applyProviderProfileToProcessEnv } =
|
||||
await importFreshProviderProfileModules()
|
||||
@@ -2912,6 +2951,147 @@ describe('applyActiveProviderProfileFromConfig', () => {
|
||||
expect(process.env.OPENAI_MODEL!).toBe('kimi-k2.6')
|
||||
})
|
||||
|
||||
test('reconciliation restores a cleared MINIMAX_API_KEY (dedicated cred is part of the alignment contract)', async () => {
|
||||
// P2b: MiniMax's transport needs the dedicated key. After activation, if
|
||||
// MINIMAX_API_KEY drifts (cleared/replaced), the env is no longer aligned
|
||||
// with the profile, so reconciliation must reapply and restore it. The
|
||||
// alignment check previously ignored MINIMAX_API_KEY, so it considered the
|
||||
// env aligned and left the wrong/absent credential in place.
|
||||
const { applyProviderProfileToProcessEnv, applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderProfileModules()
|
||||
const profile = buildProfile({
|
||||
id: 'minimax_generic',
|
||||
name: 'MiniMax generic',
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://api.minimax.io/v1',
|
||||
model: 'MiniMax-M2',
|
||||
apiKey: 'minimax-real-key',
|
||||
})
|
||||
|
||||
applyProviderProfileToProcessEnv(profile)
|
||||
expect(process.env.MINIMAX_API_KEY).toBe('minimax-real-key')
|
||||
|
||||
// Credential drift after activation.
|
||||
delete process.env.MINIMAX_API_KEY
|
||||
|
||||
const applied = applyActiveProviderProfileFromConfig({
|
||||
providerProfiles: [profile],
|
||||
activeProviderProfileId: 'minimax_generic',
|
||||
} as any)
|
||||
|
||||
expect(applied?.id).toBe('minimax_generic')
|
||||
// Cast past control-flow narrowing: TS pins this to `undefined` after the
|
||||
// delete above, since the reconciliation call is opaque to it.
|
||||
expect(process.env.MINIMAX_API_KEY as string | undefined).toBe(
|
||||
'minimax-real-key',
|
||||
)
|
||||
})
|
||||
|
||||
for (const { baseUrl, key } of [
|
||||
{ baseUrl: 'https://api.x.ai/v1', key: 'XAI_API_KEY' },
|
||||
{ baseUrl: 'https://api.venice.ai/api/v1', key: 'VENICE_API_KEY' },
|
||||
{ baseUrl: 'https://api.xiaomimimo.com/v1', key: 'MIMO_API_KEY' },
|
||||
]) {
|
||||
test(`reconciliation clears unexpected ${key} for a keyless profile`, async () => {
|
||||
const { applyProviderProfileToProcessEnv, applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderProfileModules()
|
||||
const profile = buildProfile({ provider: 'openai', baseUrl, apiKey: undefined })
|
||||
const restoreEnv = snapshotEnv([key])
|
||||
try {
|
||||
applyProviderProfileToProcessEnv(profile)
|
||||
expect(process.env[key]).toBeUndefined()
|
||||
process.env[key] = 'unexpected-key'
|
||||
|
||||
applyActiveProviderProfileFromConfig({
|
||||
...realConfig.getGlobalConfig(),
|
||||
providerProfiles: [profile],
|
||||
activeProviderProfileId: profile.id,
|
||||
})
|
||||
|
||||
expect(process.env[key]).toBeUndefined()
|
||||
} finally {
|
||||
restoreEnv()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('reconciliation keeps an explicit MiniMax route credential on a proxy host', async () => {
|
||||
const { applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderProfileModules()
|
||||
const { buildLaunchEnv, createProfileFile } = await import(
|
||||
'./providerProfile.js'
|
||||
)
|
||||
const profile = buildProfile({
|
||||
id: 'minimax_proxy',
|
||||
name: 'MiniMax proxy',
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://minimax-proxy.example.com/v1',
|
||||
model: 'MiniMax-M2.5',
|
||||
apiKey: 'correct-key',
|
||||
})
|
||||
const launch = await buildLaunchEnv({
|
||||
profile: 'openai',
|
||||
goal: 'coding',
|
||||
processEnv: {},
|
||||
persisted: createProfileFile('openai', {
|
||||
OPENAI_BASE_URL: profile.baseUrl,
|
||||
OPENAI_MODEL: profile.model,
|
||||
CLAUDE_CODE_PROVIDER_ROUTE_ID: 'minimax',
|
||||
OPENAI_API_KEY: 'correct-key',
|
||||
MINIMAX_API_KEY: 'correct-key',
|
||||
}),
|
||||
})
|
||||
const restoreEnv = snapshotEnv([
|
||||
...Object.keys(process.env),
|
||||
...Object.keys(launch),
|
||||
])
|
||||
try {
|
||||
Object.assign(process.env, launch)
|
||||
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED = '1'
|
||||
process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID = profile.id
|
||||
process.env.OPENAI_API_KEY = 'drift-openai'
|
||||
process.env.MINIMAX_API_KEY = 'wrong-minimax'
|
||||
|
||||
applyActiveProviderProfileFromConfig({
|
||||
providerProfiles: [profile],
|
||||
activeProviderProfileId: profile.id,
|
||||
} as any)
|
||||
|
||||
expect(process.env.OPENAI_API_KEY).toBe('correct-key')
|
||||
expect(process.env.MINIMAX_API_KEY).toBe('correct-key')
|
||||
expect(process.env.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe('minimax')
|
||||
expect(
|
||||
resolveRouteCredentialValue({
|
||||
routeId: 'minimax',
|
||||
baseUrl: process.env.OPENAI_BASE_URL,
|
||||
}),
|
||||
).toBe('correct-key')
|
||||
} finally {
|
||||
restoreEnv()
|
||||
}
|
||||
})
|
||||
|
||||
test('reconciliation never mirrors a MiniMax key for a lookalike host', async () => {
|
||||
const { applyProviderProfileToProcessEnv, applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderProfileModules()
|
||||
const profile = buildProfile({
|
||||
id: 'minimax_lookalike',
|
||||
name: 'MiniMax lookalike',
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://api.minimax.io.attacker.example/v1',
|
||||
model: 'gpt-4o',
|
||||
apiKey: 'attacker-key',
|
||||
})
|
||||
|
||||
applyProviderProfileToProcessEnv(profile)
|
||||
applyActiveProviderProfileFromConfig({
|
||||
providerProfiles: [profile],
|
||||
activeProviderProfileId: 'minimax_lookalike',
|
||||
} as any)
|
||||
|
||||
expect(process.env.MINIMAX_API_KEY).toBeUndefined()
|
||||
})
|
||||
|
||||
test('still respects complete shell selection with USE flag + BASE_URL', async () => {
|
||||
// Counter-example: when the user really did set both the flag AND a
|
||||
// concrete BASE_URL, that IS explicit intent and wins over the saved
|
||||
@@ -3888,6 +4068,84 @@ describe('getProviderPresetDefaults', () => {
|
||||
})
|
||||
|
||||
describe('setActiveProviderProfile', () => {
|
||||
// P2a: a keyed generic OpenAI-compatible profile on a canonical dedicated
|
||||
// host must persist its dedicated credential in the startup file, so a
|
||||
// restart re-authenticates. The strict-env branch previously returned before
|
||||
// mirroring these, so Venice persisted only OPENAI_API_KEY and MiniMax had no
|
||||
// mirror at all.
|
||||
for (const vendor of [
|
||||
{ name: 'Venice', route: 'venice', baseUrl: 'https://api.venice.ai/api/v1', key: 'VENICE_API_KEY' },
|
||||
{ name: 'MiniMax', route: 'minimax', baseUrl: 'https://api.minimax.io/v1', key: 'MINIMAX_API_KEY' },
|
||||
]) {
|
||||
test(`persists ${vendor.key} for a generic profile on the canonical ${vendor.name} host`, async () => {
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||
try {
|
||||
const { setActiveProviderProfile } =
|
||||
await importFreshProviderProfileModules()
|
||||
const profile = buildProfile({
|
||||
id: 'vendor_url_prof',
|
||||
name: `${vendor.name} via URL`,
|
||||
provider: 'openai',
|
||||
baseUrl: vendor.baseUrl,
|
||||
apiKey: 'vendor-url-key',
|
||||
})
|
||||
saveMockGlobalConfig(current => ({
|
||||
...current,
|
||||
providerProfiles: [profile],
|
||||
}))
|
||||
|
||||
const result = setActiveProviderProfile('vendor_url_prof', { configDir })
|
||||
const persisted = JSON.parse(
|
||||
readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'),
|
||||
)
|
||||
|
||||
expect(result?.id).toBe('vendor_url_prof')
|
||||
expect(persisted.env[vendor.key]).toBe('vendor-url-key')
|
||||
const { buildStartupEnvFromProfile } = await import('./providerProfile.js')
|
||||
const restarted = await buildStartupEnvFromProfile({ persisted, processEnv: {} })
|
||||
expect(restarted[vendor.key]).toBe('vendor-url-key')
|
||||
expect(restarted.OPENAI_API_KEY).toBe('vendor-url-key')
|
||||
expect(restarted.OPENAI_BASE_URL).toBe(vendor.baseUrl)
|
||||
expect(restarted.CLAUDE_CODE_PROVIDER_ROUTE_ID).toBe(vendor.route)
|
||||
expect(restarted.CLAUDE_CODE_USE_OPENAI).toBe('1')
|
||||
} finally {
|
||||
rmSync(configDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('does not persist a dedicated key for a canonical-lookalike host', async () => {
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||
try {
|
||||
const { setActiveProviderProfile } =
|
||||
await importFreshProviderProfileModules()
|
||||
const profile = buildProfile({
|
||||
id: 'lookalike_prof',
|
||||
name: 'MiniMax lookalike',
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://api.minimax.io.attacker.example/v1',
|
||||
apiKey: 'attacker-key',
|
||||
})
|
||||
saveMockGlobalConfig(current => ({
|
||||
...current,
|
||||
providerProfiles: [profile],
|
||||
}))
|
||||
|
||||
const result = setActiveProviderProfile('lookalike_prof', { configDir })
|
||||
const persisted = JSON.parse(
|
||||
readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'),
|
||||
)
|
||||
|
||||
expect(result?.id).toBe('lookalike_prof')
|
||||
expect(persisted.env.MINIMAX_API_KEY).toBeUndefined()
|
||||
expect(persisted.env.VENICE_API_KEY).toBeUndefined()
|
||||
} finally {
|
||||
rmSync(configDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('sets OPENAI_MODEL env var when switching to an openai-type provider', async () => {
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
||||
process.env.CLAUDE_CONFIG_DIR = configDir
|
||||
@@ -4347,6 +4605,121 @@ describe('setActiveProviderProfile', () => {
|
||||
}
|
||||
})
|
||||
|
||||
for (const source of [
|
||||
{ id: 'minimax', baseUrl: 'https://api.minimax.io/v1', model: 'MiniMax-M2.7', key: 'MINIMAX_API_KEY' },
|
||||
{ id: 'venice', baseUrl: 'https://api.venice.ai/api/v1', model: 'venice-uncensored', key: 'VENICE_API_KEY' },
|
||||
{ id: 'xiaomi-mimo', baseUrl: 'https://api.xiaomimimo.com/v1', model: 'mimo-v2.5-pro', key: 'MIMO_API_KEY' },
|
||||
{ id: 'xiaomi-mimo-token', baseUrl: 'https://token-plan-sgp.xiaomimimo.com/v1', model: 'mimo-v2.5-pro', key: 'MIMO_API_KEY' },
|
||||
]) {
|
||||
test(`saved generic ${source.id} launch and pane teammate authenticate with the selected provider key`, async () => {
|
||||
const {
|
||||
setActiveProviderProfile,
|
||||
applyActiveProviderProfileFromConfig,
|
||||
clearProviderProfileEnvFromProcessEnv,
|
||||
} = await importFreshProviderProfileModules()
|
||||
const { buildStartupEnvFromProfile, clearManagedProfileEnv } =
|
||||
await import('./providerProfile.js')
|
||||
const { buildInheritedEnvVars } = await import('./swarm/spawnUtils.js')
|
||||
const { tryParseShellCommand } = await import('./bash/shellQuote.js')
|
||||
const profile = buildProfile({
|
||||
baseUrl: source.baseUrl, model: source.model, apiKey: 'saved-source-key',
|
||||
})
|
||||
saveMockGlobalConfig(current => ({ ...current, providerProfiles: [profile] }))
|
||||
globalThis.fetch = (async (_input, _init) => new Response(JSON.stringify({ data: [] }))) as typeof fetch
|
||||
setActiveProviderProfile(profile.id, { configDir: testConfigDir! })
|
||||
const persisted = JSON.parse(
|
||||
readFileSync(join(testConfigDir!, '.openclaude-profile.json'), 'utf8'),
|
||||
)
|
||||
const target = source.id === 'venice'
|
||||
? { baseUrl: 'https://api.xiaomimimo.com/v1', model: 'mimo-v2.5-pro', key: 'MIMO_API_KEY' }
|
||||
: { baseUrl: 'https://api.venice.ai/api/v1', model: 'venice-uncensored', key: 'VENICE_API_KEY' }
|
||||
|
||||
// Saved dedicated keys outrank ambient generic keys, including pools.
|
||||
// Exercise restarts, dedicated key rotation and endpoint overrides through
|
||||
// the same saved profile and actual client (including MiniMax's /v1 path).
|
||||
for (const launch of [
|
||||
{ target: source, shell: {}, expectedKey: 'saved-source-key' },
|
||||
{ target: source, shell: { OPENAI_API_KEY: 'shell-generic-key' }, expectedKey: 'saved-source-key' },
|
||||
{ target: source, shell: { OPENAI_API_KEYS: 'shell-pool-key-1,shell-pool-key-2' }, expectedKey: 'saved-source-key' },
|
||||
{ target: source, shell: { [source.key]: 'rotated-source-key' }, expectedKey: 'rotated-source-key' },
|
||||
{ target, shell: { OPENAI_BASE_URL: target.baseUrl, OPENAI_MODEL: target.model, [target.key]: 'selected-target-key' }, expectedKey: 'selected-target-key' },
|
||||
]) {
|
||||
const env = await buildStartupEnvFromProfile({ persisted, processEnv: launch.shell })
|
||||
const restore = snapshotEnv([
|
||||
...Object.keys(process.env), ...Object.keys(env),
|
||||
'CLAUDECODE', 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
||||
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
|
||||
])
|
||||
const hadOwnMacro = Object.hasOwn(globalThis, 'MACRO')
|
||||
const originalMacro = (globalThis as Record<string, unknown>).MACRO
|
||||
try {
|
||||
;(globalThis as Record<string, unknown>).MACRO = { VERSION: 'test-version' }
|
||||
clearManagedProfileEnv(process.env)
|
||||
Object.assign(process.env, env)
|
||||
let capturedUrl: string | undefined
|
||||
let capturedHeaders: Headers | undefined
|
||||
let capturedRequests = 0
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
capturedRequests++
|
||||
capturedUrl = String(input)
|
||||
capturedHeaders = new Headers(init?.headers)
|
||||
return new Response(JSON.stringify({
|
||||
id: 'chat-profile', object: 'chat.completion', model: launch.target.model,
|
||||
choices: [{ index: 0, message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
|
||||
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
||||
}), { headers: { 'Content-Type': 'application/json' } })
|
||||
}) as typeof fetch
|
||||
const { getAnthropicClient } = await import('../services/api/client.js')
|
||||
const assertRequest = async () => {
|
||||
const previousRequests = capturedRequests
|
||||
const client = await getAnthropicClient({ maxRetries: 0, model: launch.target.model })
|
||||
await client.beta.messages.create({
|
||||
model: launch.target.model, max_tokens: 16,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
})
|
||||
expect(capturedRequests).toBe(previousRequests + 1)
|
||||
expect(capturedUrl).toBe(`${launch.target.baseUrl}/chat/completions`)
|
||||
expect(capturedHeaders?.get(launch.target.key === 'MIMO_API_KEY' ? 'api-key' : 'authorization'))
|
||||
.toBe(launch.target.key === 'MIMO_API_KEY' ? launch.expectedKey : `Bearer ${launch.expectedKey}`)
|
||||
}
|
||||
await assertRequest()
|
||||
|
||||
// PaneBackendExecutor sends only these explicit assignments to a
|
||||
// tmux/login shell; it cannot rely on the parent's other env vars.
|
||||
const parsed = tryParseShellCommand(buildInheritedEnvVars())
|
||||
if (!parsed.success) throw new Error(parsed.error)
|
||||
const teammateEnv = Object.fromEntries(parsed.tokens.map(token => {
|
||||
if (typeof token !== 'string' || !token.includes('=')) {
|
||||
throw new Error('Expected a teammate environment assignment')
|
||||
}
|
||||
const separator = token.indexOf('=')
|
||||
return [token.slice(0, separator), token.slice(separator + 1)]
|
||||
}))
|
||||
// Follow the child's plural-profile then startup-file stages. Its
|
||||
// complete incoming selection must preserve the parent's credential.
|
||||
expect(applyActiveProviderProfileFromConfig({
|
||||
...realConfig.getGlobalConfig(),
|
||||
providerProfiles: [profile],
|
||||
activeProviderProfileId: profile.id,
|
||||
}, { processEnv: teammateEnv })).toBeUndefined()
|
||||
const childEnv = { ...await buildStartupEnvFromProfile({
|
||||
persisted, processEnv: teammateEnv,
|
||||
}) }
|
||||
clearProviderProfileEnvFromProcessEnv()
|
||||
Object.assign(process.env, childEnv)
|
||||
await assertRequest()
|
||||
} finally {
|
||||
if (hadOwnMacro) {
|
||||
;(globalThis as Record<string, unknown>).MACRO = originalMacro
|
||||
} else {
|
||||
delete (globalThis as Record<string, unknown>).MACRO
|
||||
}
|
||||
restore()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('persists Venice profiles using a legacy-compatible openai startup profile', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
|
||||
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
|
||||
|
||||
+164
-40
@@ -65,7 +65,9 @@ import {
|
||||
isFireworksBaseUrl,
|
||||
isGmiCloudBaseUrl,
|
||||
isLongcatBaseUrl,
|
||||
isMiniMaxBaseUrl,
|
||||
isNearaiBaseUrl,
|
||||
isVeniceBaseUrl,
|
||||
isXaiBaseUrl,
|
||||
isXiaomiMimoBaseUrl,
|
||||
resolveEnvOnlyProviderRouteId,
|
||||
@@ -792,6 +794,125 @@ function serializeProfileContextWindows(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated provider credential env vars a profile's API key must be mirrored
|
||||
* into, for the family of vendors detected by exact base-URL host (and, where
|
||||
* the caller has it, the resolved route id / vendor id). Single source of truth
|
||||
* so live application, startup-env persistence, and env/profile alignment agree
|
||||
* on which host owns which dedicated key — previously each path reconstructed
|
||||
* the mapping independently and drifted (MiniMax was absent from the startup
|
||||
* builder and the alignment check; MiMo used a substring match). Credentials
|
||||
* detected by route id or profile shape (aimlapi, apismart, atlas, cloudflare,
|
||||
* bankr, nvidia, …) keep their own predicates at each site.
|
||||
*/
|
||||
const HOST_DEDICATED_ROUTE_IDS = new Set([
|
||||
'minimax',
|
||||
'venice',
|
||||
'xai',
|
||||
'xiaomi-mimo',
|
||||
'xiaomi-mimo-token',
|
||||
])
|
||||
|
||||
/**
|
||||
* Route identity already established for this exact profile in the managed
|
||||
* process env (launch reconstruction). Honor it on reconcile reapply when the
|
||||
* profile URL does not resolve to a different provider. Do not infer it from
|
||||
* a URL substring, and do not copy it onto a different profile.
|
||||
*/
|
||||
function managedExplicitHostRouteId(
|
||||
profile: ProviderProfile,
|
||||
processEnv: NodeJS.ProcessEnv,
|
||||
): string | undefined {
|
||||
if (trimOrUndefined(processEnv[PROFILE_ENV_APPLIED_ID]) !== profile.id) {
|
||||
return undefined
|
||||
}
|
||||
const routeId = trimOrUndefined(processEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID)
|
||||
if (!routeId || !HOST_DEDICATED_ROUTE_IDS.has(routeId)) {
|
||||
return undefined
|
||||
}
|
||||
const resolvedFromUrl = resolveRouteIdFromBaseUrl(profile.baseUrl)
|
||||
if (
|
||||
resolvedFromUrl &&
|
||||
resolvedFromUrl !== 'openai' &&
|
||||
resolvedFromUrl !== routeId
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
return routeId
|
||||
}
|
||||
|
||||
function hostDedicatedRouteForProfile(
|
||||
profile: ProviderProfile,
|
||||
processEnv: NodeJS.ProcessEnv,
|
||||
profileRoute: { routeId?: string; vendorId?: string },
|
||||
): { routeId?: string; vendorId?: string } {
|
||||
const managedRouteId = managedExplicitHostRouteId(profile, processEnv)
|
||||
if (
|
||||
!managedRouteId ||
|
||||
(profileRoute.routeId && HOST_DEDICATED_ROUTE_IDS.has(profileRoute.routeId))
|
||||
) {
|
||||
return profileRoute
|
||||
}
|
||||
return { routeId: managedRouteId, vendorId: profileRoute.vendorId }
|
||||
}
|
||||
|
||||
function getHostDedicatedCredentialEnv(
|
||||
baseUrl: string | undefined,
|
||||
apiKey: string,
|
||||
route?: { routeId?: string; vendorId?: string },
|
||||
): Record<string, string> {
|
||||
const env: Record<string, string> = {}
|
||||
if (route?.routeId === 'xai' || isXaiBaseUrl(baseUrl)) {
|
||||
env.XAI_API_KEY = apiKey
|
||||
}
|
||||
if (
|
||||
route?.routeId === 'minimax' ||
|
||||
route?.vendorId === 'minimax' ||
|
||||
isMiniMaxBaseUrl(baseUrl)
|
||||
) {
|
||||
env.MINIMAX_API_KEY = apiKey
|
||||
}
|
||||
if (route?.routeId === 'venice' || isVeniceBaseUrl(baseUrl)) {
|
||||
env.VENICE_API_KEY = apiKey
|
||||
}
|
||||
if (
|
||||
route?.routeId === 'xiaomi-mimo' ||
|
||||
route?.routeId === 'xiaomi-mimo-token' ||
|
||||
isXiaomiMimoBaseUrl(baseUrl)
|
||||
) {
|
||||
env.MIMO_API_KEY = apiKey
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the process env carries the profile-owned value for every dedicated
|
||||
* host credential the profile expects. Uses getHostDedicatedCredentialEnv so
|
||||
* the alignment contract can never fall behind what application/persistence
|
||||
* mirror, including absent credentials on keyless profiles.
|
||||
*/
|
||||
function hostDedicatedCredentialsAligned(
|
||||
processEnv: NodeJS.ProcessEnv,
|
||||
profile: ProviderProfile,
|
||||
includeApiKey: boolean,
|
||||
): boolean {
|
||||
if (!includeApiKey) {
|
||||
return true
|
||||
}
|
||||
const dedicated = getHostDedicatedCredentialEnv(
|
||||
profile.baseUrl,
|
||||
profile.apiKey ?? '',
|
||||
hostDedicatedRouteForProfile(
|
||||
profile,
|
||||
processEnv,
|
||||
resolveProfileRoute(profile.provider),
|
||||
),
|
||||
)
|
||||
return Object.entries(dedicated).every(([key, value]) =>
|
||||
sameOptionalEnvValue(processEnv[key], value),
|
||||
)
|
||||
}
|
||||
|
||||
function isProcessEnvAlignedWithProfile(
|
||||
processEnv: NodeJS.ProcessEnv,
|
||||
profile: ProviderProfile,
|
||||
@@ -945,23 +1066,11 @@ function isProcessEnvAlignedWithProfile(
|
||||
? !includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.BNKR_API_KEY, profile.apiKey)
|
||||
: true) &&
|
||||
(isXaiBaseUrl(profile.baseUrl)
|
||||
? !includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.XAI_API_KEY, profile.apiKey)
|
||||
: true) &&
|
||||
hostDedicatedCredentialsAligned(processEnv, profile, includeApiKey) &&
|
||||
(isAimlapiRoute
|
||||
? !includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.AIMLAPI_API_KEY, profile.apiKey)
|
||||
: true) &&
|
||||
(profile.baseUrl?.toLowerCase().includes('api.venice.ai')
|
||||
? !includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.VENICE_API_KEY, profile.apiKey)
|
||||
: true) &&
|
||||
(profile.baseUrl?.toLowerCase().includes('api.xiaomimimo.com') ||
|
||||
profile.baseUrl?.toLowerCase().includes('api.mimo-v2.com')
|
||||
? !includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.MIMO_API_KEY, profile.apiKey)
|
||||
: true) &&
|
||||
(profile.baseUrl?.toLowerCase().includes('atlascloud')
|
||||
? !includeApiKey ||
|
||||
sameOptionalEnvValue(processEnv.ATLAS_CLOUD_API_KEY, profile.apiKey)
|
||||
@@ -1206,8 +1315,30 @@ export function applyProviderProfileToProcessEnv(
|
||||
!withholdRetargetedGmiCloudCredential
|
||||
) {
|
||||
openAIProfileEnv.OPENAI_API_KEY = profile.apiKey
|
||||
if (route.vendorId === 'minimax' || normalizedProfileBaseUrl.toLowerCase().includes('minimax')) {
|
||||
openAIProfileEnv.MINIMAX_API_KEY = profile.apiKey
|
||||
// Host/route-detected dedicated keys (xai, minimax, venice, mimo) come
|
||||
// from the shared mapping so this path, startup persistence, and env
|
||||
// alignment stay in lockstep. A generic OpenAI profile keeps an explicit
|
||||
// host route only when this same profile's managed env already has it
|
||||
// (launch reconstruction); reconcile must not drop that credential.
|
||||
const dedicatedRoute = hostDedicatedRouteForProfile(
|
||||
profile,
|
||||
process.env,
|
||||
route,
|
||||
)
|
||||
Object.assign(
|
||||
openAIProfileEnv,
|
||||
getHostDedicatedCredentialEnv(
|
||||
profile.baseUrl,
|
||||
profile.apiKey,
|
||||
dedicatedRoute,
|
||||
),
|
||||
)
|
||||
if (
|
||||
dedicatedRoute.routeId &&
|
||||
HOST_DEDICATED_ROUTE_IDS.has(dedicatedRoute.routeId) &&
|
||||
route.routeId !== dedicatedRoute.routeId
|
||||
) {
|
||||
openAIProfileEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = dedicatedRoute.routeId
|
||||
}
|
||||
if (
|
||||
route.gatewayId === 'nvidia-nim' ||
|
||||
@@ -1219,22 +1350,9 @@ export function applyProviderProfileToProcessEnv(
|
||||
if (route.routeId === 'bankr' || normalizedProfileBaseUrl.toLowerCase().includes('bankr')) {
|
||||
openAIProfileEnv.BNKR_API_KEY = profile.apiKey
|
||||
}
|
||||
if (route.routeId === 'xai' || isXaiBaseUrl(profile.baseUrl)) {
|
||||
openAIProfileEnv.XAI_API_KEY = profile.apiKey
|
||||
}
|
||||
if (isAimlapiProfile) {
|
||||
openAIProfileEnv.AIMLAPI_API_KEY = profile.apiKey
|
||||
}
|
||||
if (route.routeId === 'venice' || normalizedProfileBaseUrl.toLowerCase().includes('api.venice.ai')) {
|
||||
openAIProfileEnv.VENICE_API_KEY = profile.apiKey
|
||||
}
|
||||
if (
|
||||
route.routeId === 'xiaomi-mimo' ||
|
||||
route.routeId === 'xiaomi-mimo-token' ||
|
||||
isXiaomiMimoBaseUrl(profile.baseUrl)
|
||||
) {
|
||||
openAIProfileEnv.MIMO_API_KEY = profile.apiKey
|
||||
}
|
||||
if (route.routeId === 'atlas-cloud' || normalizedProfileBaseUrl.toLowerCase().includes('atlascloud')) {
|
||||
openAIProfileEnv.ATLAS_CLOUD_API_KEY = profile.apiKey
|
||||
}
|
||||
@@ -1739,6 +1857,17 @@ function buildOpenAICompatibleStartupEnv(
|
||||
activeProfile.maxContextLength,
|
||||
)
|
||||
}
|
||||
// The strict env omitted the host-detected dedicated keys, so a keyed
|
||||
// generic profile on a canonical MiniMax/Venice/xAI/MiMo host persisted
|
||||
// only OPENAI_API_KEY and relaunched without its dedicated credential.
|
||||
// Mirror them from the shared mapping, same as the fallback path below.
|
||||
Object.assign(
|
||||
strictEnv,
|
||||
getHostDedicatedCredentialEnv(
|
||||
activeProfile.baseUrl,
|
||||
activeProfile.apiKey,
|
||||
),
|
||||
)
|
||||
if (isAimlapiProfile) {
|
||||
strictEnv.AIMLAPI_API_KEY = activeProfile.apiKey
|
||||
strictEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'aimlapi'
|
||||
@@ -1858,24 +1987,19 @@ function buildOpenAICompatibleStartupEnv(
|
||||
!withholdRetargetedCommandcodeCredential
|
||||
) {
|
||||
env.OPENAI_API_KEY = activeProfile.apiKey
|
||||
// Host-detected dedicated keys (xai, minimax, venice, mimo) from the shared
|
||||
// mapping — this adds the previously-absent MiniMax mirror and replaces the
|
||||
// MiMo substring match with the exact-host predicate.
|
||||
Object.assign(
|
||||
env,
|
||||
getHostDedicatedCredentialEnv(activeProfile.baseUrl, activeProfile.apiKey),
|
||||
)
|
||||
if (activeProfile.baseUrl?.toLowerCase().includes('bankr')) {
|
||||
env.BNKR_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (isXaiBaseUrl(activeProfile.baseUrl)) {
|
||||
env.XAI_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (isAimlapiProfile) {
|
||||
env.AIMLAPI_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (activeProfile.baseUrl?.toLowerCase().includes('api.venice.ai')) {
|
||||
env.VENICE_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (
|
||||
activeProfile.baseUrl?.toLowerCase().includes('api.xiaomimimo.com') ||
|
||||
activeProfile.baseUrl?.toLowerCase().includes('api.mimo-v2.com')
|
||||
) {
|
||||
env.MIMO_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
if (activeProfile.baseUrl?.toLowerCase().includes('atlascloud')) {
|
||||
env.ATLAS_CLOUD_API_KEY = activeProfile.apiKey
|
||||
}
|
||||
|
||||
@@ -109,6 +109,11 @@ const TEAMMATE_ENV_VARS = [
|
||||
'GH_TOKEN',
|
||||
'OPENAI_API_KEYS',
|
||||
'OPENAI_API_KEY',
|
||||
// Saved-profile launches can select these ahead of ambient generic keys.
|
||||
// Preserve that selection in the pane's explicit environment.
|
||||
'MINIMAX_API_KEY',
|
||||
'VENICE_API_KEY',
|
||||
'MIMO_API_KEY',
|
||||
'LLMTR_API_KEY',
|
||||
'MERGE_GATEWAY_API_KEY',
|
||||
'CMD_API_KEY',
|
||||
|
||||
Reference in New Issue
Block a user