diff --git a/src/integrations/providerUiMetadata.ts b/src/integrations/providerUiMetadata.ts index d32ea9368..574ddb1f3 100644 --- a/src/integrations/providerUiMetadata.ts +++ b/src/integrations/providerUiMetadata.ts @@ -45,6 +45,7 @@ function hasUsableEnvValue(envVar: string, value: string | undefined): boolean { envVar === 'OPENAI_API_KEYS' || envVar === 'OPENAI_API_KEY' || envVar === 'LLMTR_API_KEY' || + envVar === 'REQUESTY_API_KEY' || envVar === 'MERGE_GATEWAY_API_KEY' || envVar === 'CMD_API_KEY' || envVar === 'COMMANDCODE_API_KEY' || diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index dffc62452..c1a78cffc 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -245,6 +245,7 @@ function hasUsableEnvCredentialValue( envVar === 'APISMART_API_KEY' || envVar === 'CONCENTRATE_API_KEY' || envVar === 'LLMTR_API_KEY' || + envVar === 'REQUESTY_API_KEY' || envVar === 'MERGE_GATEWAY_API_KEY' || envVar === 'CMD_API_KEY' || envVar === 'COMMANDCODE_API_KEY' || diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index 4feb3c8e2..a6266a093 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -3990,3 +3990,40 @@ test('a shell URL retarget onto canonical Requesty drops a saved proxy profile c assert.equal(explicit.REQUESTY_API_KEY, 'shell-requesty-key') assert.equal(explicit.OPENAI_API_KEY, undefined) }) + +test('a saved Requesty proxy profile withholds ambient credentials on restart', async () => { + // The startup file a Requesty proxy profile writes: route identity, its own + // URL and its own custom auth, but no saved key. + const persisted = profile('openai', { + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'requesty', + OPENAI_BASE_URL: 'https://requesty-proxy.example/v1', + OPENAI_MODEL: 'openai/gpt-5-mini', + OPENAI_AUTH_HEADER: 'X-Proxy-Auth', + OPENAI_AUTH_SCHEME: 'raw', + OPENAI_AUTH_HEADER_VALUE: 'saved-proxy-secret', + }) + const ambient = { + OPENAI_API_KEY: 'ambient-requesty-key', + OPENAI_API_KEYS: 'ambient-requesty-key', + REQUESTY_API_KEY: 'ambient-requesty-key', + } + + for (const processEnv of [ + ambient, + // A concrete shell selection must not skip the saved profile's boundary. + { + ...ambient, + CLAUDE_CODE_USE_OPENAI: '1', + OPENAI_BASE_URL: 'https://requesty-proxy.example/v1', + OPENAI_MODEL: 'openai/gpt-5-mini', + }, + ]) { + const env = await buildStartupEnvFromProfile({ persisted, processEnv }) + assert.equal(env.OPENAI_BASE_URL, 'https://requesty-proxy.example/v1') + assert.equal(env.CLAUDE_CODE_PROVIDER_ROUTE_ID, 'requesty') + assert.equal(env.OPENAI_API_KEY, undefined) + assert.equal(env.OPENAI_API_KEYS, undefined) + assert.equal(env.REQUESTY_API_KEY, undefined) + assert.equal(env.OPENAI_AUTH_HEADER_VALUE, 'saved-proxy-secret') + } +}) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 96d97a9df..520e2c725 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -2373,6 +2373,10 @@ export async function buildLaunchEnv(options: { effectiveOpenAIRouteId === 'llmtr' && !!env.OPENAI_BASE_URL?.trim() && !isCanonicalLlmtrInferenceBaseUrl(env.OPENAI_BASE_URL) + const isNoncanonicalRequestyLaunch = + effectiveOpenAIRouteId === 'requesty' && + !!env.OPENAI_BASE_URL?.trim() && + !isCanonicalRequestyInferenceBaseUrl(env.OPENAI_BASE_URL) const isNoncanonicalMergeGatewayLaunch = effectiveOpenAIRouteId === 'merge-gateway' && !!env.OPENAI_BASE_URL?.trim() && @@ -2390,6 +2394,7 @@ export async function buildLaunchEnv(options: { isNoncanonicalApismartLaunch || isNoncanonicalConcentrateLaunch || isNoncanonicalLlmtrLaunch || + isNoncanonicalRequestyLaunch || isNoncanonicalMergeGatewayLaunch || isNoncanonicalCommandcodeLaunch || isNoncanonicalApiRouteLaunch @@ -2406,6 +2411,7 @@ export async function buildLaunchEnv(options: { } else if ( !isNoncanonicalConcentrateLaunch && !isNoncanonicalLlmtrLaunch && + !isNoncanonicalRequestyLaunch && !isNoncanonicalMergeGatewayLaunch && !isNoncanonicalCommandcodeLaunch ) { @@ -2889,6 +2895,11 @@ export async function buildStartupEnvFromProfile(options?: { persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'llmtr' && !!persisted.env.OPENAI_BASE_URL?.trim() && !isCanonicalLlmtrInferenceBaseUrl(persisted.env.OPENAI_BASE_URL) + const persistedRequestyProxy = + persisted?.profile === 'openai' && + persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'requesty' && + !!persisted.env.OPENAI_BASE_URL?.trim() && + !isCanonicalRequestyInferenceBaseUrl(persisted.env.OPENAI_BASE_URL) const persistedMergeGatewayProxy = persisted?.profile === 'openai' && persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'merge-gateway' && @@ -2909,6 +2920,7 @@ export async function buildStartupEnvFromProfile(options?: { !persistedApismartProxy && !persistedConcentrateProxy && !persistedLlmtrProxy && + !persistedRequestyProxy && !persistedMergeGatewayProxy && !persistedCommandcodeProxy && !persistedApiRouteProxy diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index f3ff4fd92..5864ec266 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -618,6 +618,29 @@ test('noncanonical Requesty host paths do not validate a dedicated credential', await expect(getProviderValidationError(process.env)).resolves.toBeNull() }) +test.each([undefined, '', 'undefined', 'null', 'SUA_CHAVE', 'requesty-key'])('Requesty validation, picker and runtime agree on usable dedicated credentials: %s', async key => { + const { getProviderPresetUiMetadata } = await import('../integrations/providerUiMetadata.js') + const { resolveRouteCredentialValue } = await import('../integrations/routeMetadata.js') + const usable = key === 'requesty-key' + const env: NodeJS.ProcessEnv = { + CLAUDE_CODE_USE_OPENAI: '1', OPENAI_MODEL: 'openai/gpt-5-mini', + OPENAI_BASE_URL: 'https://router.requesty.ai/v1', REQUESTY_API_KEY: key, + } + const credential = () => resolveRouteCredentialValue({ routeId: 'requesty', baseUrl: env.OPENAI_BASE_URL, processEnv: env }) + + expect((await getProviderValidationError(env)) === null).toBe(usable) + expect(credential()).toBe(usable ? key : undefined) + expect(getProviderPresetUiMetadata('requesty', env).apiKey).toBe(usable ? key : '') + + // An unusable dedicated key falls back to a usable generic credential. + env.OPENAI_API_KEY = 'openai-fallback-key' + await expect(getProviderValidationError(env)).resolves.toBeNull() + expect(credential()).toBe(usable ? key : 'openai-fallback-key') + delete env.OPENAI_API_KEY + env.OPENAI_API_KEYS = 'pool-key-a,pool-key-b' + await expect(getProviderValidationError(env)).resolves.toBeNull() +}) + test('noncanonical Concentrate host paths fall back to generic OpenAI validation', async () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.OPENAI_BASE_URL = 'https://api.concentrate.ai/staging/v1' diff --git a/src/utils/providerValidation.ts b/src/utils/providerValidation.ts index 0af3d4f5a..ba1113895 100644 --- a/src/utils/providerValidation.ts +++ b/src/utils/providerValidation.ts @@ -158,6 +158,7 @@ function hasUsableCredentialEnvValue( envVar === 'API_ROUTE_API_KEY' || envVar === 'CONCENTRATE_API_KEY' || envVar === 'LLMTR_API_KEY' || + envVar === 'REQUESTY_API_KEY' || envVar === 'MERGE_GATEWAY_API_KEY' || envVar === 'CMD_API_KEY' || envVar === 'COMMANDCODE_API_KEY' ||