From 41f409bf8a64fb3969056d51976f7630c561a4f7 Mon Sep 17 00:00:00 2001 From: jatmn Date: Wed, 4 Nov 2026 16:03:24 -0800 Subject: [PATCH] fix(providers): keep saved Requesty keys off shell-retargeted launches A relaunch with a shell OPENAI_BASE_URL that points a saved Requesty profile at a non-canonical URL still carried the profile's persisted OPENAI_API_KEY to that URL. Drop persisted generic credentials for that retarget, as the Merge and Runcrate guards already do; explicit shell credentials still apply. Also require the persisted profile to be a Requesty profile before its generic key is recovered as REQUESTY_API_KEY, so a shell retarget of another profile to Requesty no longer adopts that profile's key over a shell REQUESTY_API_KEY. Co-Authored-By: Claude Opus 5.5 --- src/utils/providerProfile.test.ts | 60 +++++++++++++++++++++++++++++++ src/utils/providerProfile.ts | 19 ++++++++-- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index cf182ec4a..5f378fa26 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -3811,3 +3811,63 @@ test('OpenAI profile builder materializes the canonical Orca key only at its rou }) assert.equal(custom?.ORCAROUTER_API_KEY, undefined) }) + +test('a shell URL retarget does not carry a saved Requesty key to the new endpoint', async () => { + const persisted = profile('openai', { + OPENAI_BASE_URL: 'https://router.requesty.ai/v1', + OPENAI_MODEL: 'openai/gpt-5-mini', + OPENAI_API_KEY: 'saved-requesty-key', + REQUESTY_API_KEY: 'saved-requesty-key', + }) + + for (const shellBaseUrl of [ + 'https://example.test/v1', + 'http://router.requesty.ai/v1', + ]) { + const env = await buildLaunchEnv({ + profile: 'openai', + persisted, + goal: 'balanced', + processEnv: { OPENAI_BASE_URL: shellBaseUrl }, + }) + + assert.equal(env.OPENAI_BASE_URL, shellBaseUrl) + assert.equal(env.OPENAI_API_KEY, undefined) + assert.equal(env.REQUESTY_API_KEY, undefined) + } + + const regional = await buildLaunchEnv({ + profile: 'openai', + persisted, + goal: 'balanced', + processEnv: { OPENAI_BASE_URL: 'https://router.eu.requesty.ai/v1' }, + }) + assert.equal(regional.OPENAI_API_KEY, 'saved-requesty-key') + assert.equal(regional.REQUESTY_API_KEY, 'saved-requesty-key') +}) + +test('a shell URL retarget to Requesty does not adopt another profile key as the Requesty key', async () => { + const env = await buildLaunchEnv({ + profile: 'openai', + persisted: profile('openai', { + OPENAI_BASE_URL: 'https://api.openai.com/v1', + OPENAI_MODEL: 'gpt-4o', + OPENAI_API_KEY: 'sk-saved-openai', + }), + goal: 'balanced', + processEnv: { + OPENAI_BASE_URL: 'https://router.requesty.ai/v1', + REQUESTY_API_KEY: 'shell-requesty-key', + }, + }) + + assert.equal(env.REQUESTY_API_KEY, 'shell-requesty-key') + assert.equal( + resolveRouteCredentialValue({ + routeId: 'requesty', + baseUrl: env.OPENAI_BASE_URL, + processEnv: env, + }), + 'shell-requesty-key', + ) +}) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index c56856470..2b19e6467 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -2260,6 +2260,14 @@ export async function buildLaunchEnv(options: { (persistedEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'merge-gateway' || isCanonicalMergeGatewayInferenceBaseUrl(persistedOpenAIBaseUrl)) && !isCanonicalMergeGatewayInferenceBaseUrl(env.OPENAI_BASE_URL) + // Same for a saved Requesty profile: its key is valid only on the canonical + // Requesty inference URLs, so a shell retarget elsewhere drops it. + const persistedRequestyProfileIdentity = + persistedEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'requesty' || + isCanonicalRequestyInferenceBaseUrl(persistedOpenAIBaseUrl) + const retargetedRequestyProfile = + persistedRequestyProfileIdentity && + !isCanonicalRequestyInferenceBaseUrl(env.OPENAI_BASE_URL) // A saved Runcrate profile mirrors its dedicated key into the generic // OPENAI_API_KEY alias. A shell URL retarget must not hand that saved key to // the new endpoint; only an explicit shell credential may follow the URL. @@ -2283,7 +2291,11 @@ export async function buildLaunchEnv(options: { : processEnv const openAICredential = resolveOpenAICredentialEnvOverride( shellCredentialSource, - retargetedMergeProfile || retargetedRuncrateProfile ? {} : persistedEnv, + retargetedMergeProfile || + retargetedRuncrateProfile || + retargetedRequestyProfile + ? {} + : persistedEnv, ) if (openAICredential) { env[openAICredential.envVar] = openAICredential.value @@ -2632,10 +2644,13 @@ export async function buildLaunchEnv(options: { : undefined) : undefined // Same ownership rule for a selected canonical Requesty profile, including - // startup files that only persisted the saved key as OPENAI_API_KEY. + // startup files that only persisted the saved key as OPENAI_API_KEY. Only + // a persisted Requesty profile owns that key; another profile's generic + // key is not a Requesty credential. const persistedRequestyProfileKey = dedicatedKey === 'REQUESTY_API_KEY' && effectiveOpenAIRouteId === 'requesty' && + persistedRequestyProfileIdentity && !!dedicatedBaseUrl && isCanonicalRequestyInferenceBaseUrl(dedicatedBaseUrl) ? sanitizeApiKey(persistedEnv.REQUESTY_API_KEY) ||