From 7d3dbb86a5bdcee566422fda15a9e5307eccbdd5 Mon Sep 17 00:00:00 2001 From: jatmn Date: Wed, 4 Nov 2026 16:40:41 -0800 Subject: [PATCH] fix(providers): drop saved Requesty auth and headers on shell retarget The Requesty retarget boundary only gated the generic OPENAI_API_KEY selection. Requesty supports custom auth headers, so a saved profile's credential can also ride in the OPENAI_AUTH_* trio or ANTHROPIC_CUSTOM_HEADERS, and a shell OPENAI_BASE_URL retarget still sent those to the new endpoint. Apply the existing Runcrate retarget handling to a Requesty retarget away from its saved endpoint, so every credential channel follows the same rule. A canonical regional switch and a saved non-canonical profile relaunched at its own URL keep their saved auth, matching profile apply. Co-Authored-By: Claude Opus 5.5 --- src/utils/providerProfile.test.ts | 72 +++++++++++++++++++++++++++++++ src/utils/providerProfile.ts | 25 ++++++++--- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index 5f378fa26..8b534ef5c 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -3871,3 +3871,75 @@ test('a shell URL retarget to Requesty does not adopt another profile key as the 'shell-requesty-key', ) }) + +test('a shell URL retarget drops a saved Requesty profile custom auth and headers', async () => { + // Requesty supports custom auth headers, so its saved credential can ride in + // the custom-auth trio or custom headers as well as the API key aliases. + const persisted = profile('openai', { + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'requesty', + 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', + OPENAI_AUTH_HEADER: 'X-Requesty-Auth', + OPENAI_AUTH_SCHEME: 'raw', + OPENAI_AUTH_HEADER_VALUE: 'saved-auth-secret', + ANTHROPIC_CUSTOM_HEADERS: 'X-Requesty-Auth: saved-header-secret', + }) + + for (const shellBaseUrl of [ + 'https://example.test/v1', + 'http://router.requesty.ai/v1', + ]) { + const env = await buildStartupEnvFromProfile({ + persisted, + 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) + assert.equal(env.OPENAI_AUTH_HEADER, undefined) + assert.equal(env.OPENAI_AUTH_SCHEME, undefined) + assert.equal(env.OPENAI_AUTH_HEADER_VALUE, undefined) + assert.equal(env.ANTHROPIC_CUSTOM_HEADERS, undefined) + } + + // Explicit shell credentials still follow the retargeted endpoint. + const explicit = await buildStartupEnvFromProfile({ + persisted, + processEnv: { + OPENAI_BASE_URL: 'https://example.test/v1', + OPENAI_API_KEY: 'shell-key', + OPENAI_AUTH_HEADER: 'X-Shell-Auth', + OPENAI_AUTH_SCHEME: 'bearer', + OPENAI_AUTH_HEADER_VALUE: 'shell-auth-secret', + }, + }) + assert.equal(explicit.OPENAI_API_KEY, 'shell-key') + assert.equal(explicit.OPENAI_AUTH_HEADER, 'X-Shell-Auth') + assert.equal(explicit.OPENAI_AUTH_SCHEME, 'bearer') + assert.equal(explicit.OPENAI_AUTH_HEADER_VALUE, 'shell-auth-secret') + + // A canonical regional host is still Requesty: the saved credentials follow. + const regional = await buildStartupEnvFromProfile({ + persisted, + processEnv: { OPENAI_BASE_URL: 'https://router.eu.requesty.ai/v1' }, + }) + assert.equal(regional.REQUESTY_API_KEY, 'saved-requesty-key') + assert.equal(regional.OPENAI_AUTH_HEADER_VALUE, 'saved-auth-secret') + assert.equal(regional.ANTHROPIC_CUSTOM_HEADERS, 'X-Requesty-Auth: saved-header-secret') + + // A profile saved for its own non-canonical URL keeps the auth it was saved + // with when relaunched there, matching profile apply. + const ownProxy = await buildStartupEnvFromProfile({ + persisted: profile('openai', { + ...persisted.env, + OPENAI_BASE_URL: 'https://requesty-proxy.example/v1', + OPENAI_API_KEY: undefined, + REQUESTY_API_KEY: undefined, + }), + processEnv: {}, + }) + assert.equal(ownProxy.OPENAI_AUTH_HEADER_VALUE, 'saved-auth-secret') + assert.equal(ownProxy.ANTHROPIC_CUSTOM_HEADERS, 'X-Requesty-Auth: saved-header-secret') +}) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 2b19e6467..e29d35ce7 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -2275,6 +2275,18 @@ export async function buildLaunchEnv(options: { (persistedEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'runcrate' || resolveRouteIdFromBaseUrl(persistedOpenAIBaseUrl) === 'runcrate') && !isSameConfiguredEndpoint(env.OPENAI_BASE_URL, persistedOpenAIBaseUrl) + // Requesty supports custom auth headers, so its saved credential can also + // ride in the custom-auth trio or custom headers. Those belong to the + // endpoint the profile was saved for: a shell retarget away from it drops + // them, while a saved non-canonical profile relaunched at its own URL keeps + // them, matching profile apply. + const shellRetargetedRequestyProfile = + retargetedRequestyProfile && + !isSameConfiguredEndpoint(env.OPENAI_BASE_URL, persistedOpenAIBaseUrl) + // Saved profiles whose credentials must not follow a shell URL retarget on + // any channel: generic key, custom-auth trio, or custom headers. + const retargetedCredentialBoundProfile = + retargetedRuncrateProfile || shellRetargetedRequestyProfile // A profile-applied Runcrate key also rides in the generic alias, and a pane // can inherit it. Only a shell credential without Runcrate's managed marker // may follow a retarget. @@ -2376,10 +2388,10 @@ export async function buildLaunchEnv(options: { isNoncanonicalMergeGatewayLaunch || isNoncanonicalCommandcodeLaunch || isNoncanonicalApiRouteLaunch - if (isNoncanonicalDedicatedOpenAILaunch || retargetedRuncrateProfile) { + if (isNoncanonicalDedicatedOpenAILaunch || retargetedCredentialBoundProfile) { delete env.OPENAI_API_KEY delete env.OPENAI_API_KEYS - if (retargetedRuncrateProfile) { + if (retargetedCredentialBoundProfile) { // A retargeted launch keeps no identity for the endpoint it now points // at, so only the explicit shell credential may follow it. const shellCredential = resolveOpenAICredentialEnvSelection(shellCredentialSource) @@ -2402,7 +2414,7 @@ export async function buildLaunchEnv(options: { env[persistedCredential.envVar] = persistedCredential.value } } - if (retargetedRuncrateProfile) { + if (retargetedCredentialBoundProfile) { // A retargeted launch keeps no saved custom-auth trio: it belongs to the // endpoint the profile was saved for. Provenance comes from the marker a // profile application wrote, not from the values themselves: the saved @@ -2766,9 +2778,10 @@ export async function buildLaunchEnv(options: { // and is sent on every request. So an ambient value must be withheld from a // non-canonical aimlapi/apismart launch exactly like the API key and the // custom-auth trio; only headers the profile itself persisted are restored. - // A retargeted Runcrate launch keeps no headers either: both ambient and - // persisted values belong to the endpoint the profile was saved for. - const customHeaders = retargetedRuncrateProfile + // A retargeted Runcrate or Requesty launch keeps no headers either: both + // ambient and persisted values belong to the endpoint the profile was saved + // for. + const customHeaders = retargetedCredentialBoundProfile ? undefined : isNoncanonicalDedicatedOpenAILaunch ? persistedCustomHeaders