From 894174549315cab149054441155445b84fce215b Mon Sep 17 00:00:00 2001 From: jatmn Date: Wed, 4 Nov 2026 18:10:26 -0800 Subject: [PATCH] fix(requesty): bind route identity and discovery to the canonical endpoint Requesty's credential was bound to the canonical https /v1 hosts, but the sibling sites that apply the same boundary for LLMTR, Merge, Command Code and API Route did not include Requesty: - URL route resolution, active-profile and capability resolution, and provider validation treated any Requesty host or retargeted profile as `requesty`, so validation accepted REQUESTY_API_KEY for a URL whose requests withhold it and capability surfaces kept the Requesty route. - Hybrid discovery forwarded a caller or saved key to a noncanonical base URL, and a retargeted profile still triggered startup discovery. - A profile saved for a noncanonical URL persisted no route identity, so the relaunch retarget guard missed it and its saved custom auth and headers followed a shell retarget onto canonical Requesty. - REQUESTY_API_KEY was not protected from settings overrides when the host manages the provider. Register Requesty at each of those sites, matching its siblings. Co-Authored-By: Claude Opus 5.5 --- src/integrations/discoveryService.test.ts | 28 +++++++ src/integrations/discoveryService.ts | 5 +- src/integrations/routeMetadata.test.ts | 28 +++++++ src/integrations/routeMetadata.ts | 13 +++- src/utils/managedEnv.test.ts | 16 ++++ src/utils/managedEnvConstants.ts | 1 + src/utils/providerProfiles.test.ts | 94 +++++++++++++++++++++++ src/utils/providerProfiles.ts | 13 +++- src/utils/providerValidation.test.ts | 22 ++++++ src/utils/providerValidation.ts | 5 +- 10 files changed, 221 insertions(+), 4 deletions(-) diff --git a/src/integrations/discoveryService.test.ts b/src/integrations/discoveryService.test.ts index 191c4fd2e..44a1edb24 100644 --- a/src/integrations/discoveryService.test.ts +++ b/src/integrations/discoveryService.test.ts @@ -26,6 +26,7 @@ const originalEnv = { OPENAI_AUTH_SCHEME: process.env.OPENAI_AUTH_SCHEME, APISMART_API_KEY: process.env.APISMART_API_KEY, MERGE_GATEWAY_API_KEY: process.env.MERGE_GATEWAY_API_KEY, + REQUESTY_API_KEY: process.env.REQUESTY_API_KEY, ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS, CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, @@ -73,6 +74,7 @@ function clearProviderEnv(): void { delete process.env.OPENAI_AUTH_SCHEME delete process.env.APISMART_API_KEY delete process.env.MERGE_GATEWAY_API_KEY + delete process.env.REQUESTY_API_KEY delete process.env.ANTHROPIC_CUSTOM_HEADERS delete process.env.CLAUDE_CODE_USE_OPENAI delete process.env.CLAUDE_CODE_USE_GEMINI @@ -237,6 +239,32 @@ describe('discoverModelsForRoute', () => { expect(authorizations).toEqual([]) }) + test('withholds explicit and ambient Requesty keys from retargeted discovery', async () => { + const { discoverModelsForRoute } = await loadDiscoveryServiceModule() + process.env.REQUESTY_API_KEY = 'requesty-secret' + const authorizations: Array = [] + setMockFetch(mock((_input: string | URL | Request, init?: RequestInit) => { + authorizations.push(new Headers(init?.headers).get('authorization')) + return Promise.resolve(new Response(JSON.stringify({ data: [] }))) + }) as unknown as typeof globalThis.fetch) + for (const baseUrl of [ + 'https://proxy.example/v1', + 'http://router.requesty.ai/v1', + ]) { + for (const apiKey of [undefined, 'requesty-secret']) { + await discoverModelsForRoute('requesty', { + baseUrl, apiKey, forceRefresh: true, + }) + } + } + expect(authorizations).toEqual([null, null, null, null]) + + await discoverModelsForRoute('requesty', { + baseUrl: 'https://router.requesty.ai/v1', forceRefresh: true, + }) + expect(authorizations.at(-1)).toBe('Bearer requesty-secret') + }) + test('caches the complete native Merge catalog after cursor continuation', async () => { const { discoverModelsForRoute } = await loadDiscoveryServiceModule() process.env.MERGE_GATEWAY_API_KEY = 'merge-key' diff --git a/src/integrations/discoveryService.ts b/src/integrations/discoveryService.ts index 2e92208d0..3f0b8a73f 100644 --- a/src/integrations/discoveryService.ts +++ b/src/integrations/discoveryService.ts @@ -17,6 +17,7 @@ import { getRouteDescriptor, isCanonicalApismartInferenceBaseUrl, isCanonicalMergeGatewayInferenceBaseUrl, + isCanonicalRequestyInferenceBaseUrl, isCanonicalXaiInferenceBaseUrl, resolveActiveRouteIdFromEnv, resolveRouteCredentialValue, @@ -228,7 +229,9 @@ function getRouteDiscoveryApiKey( (routeId === 'apismart' && !isCanonicalApismartInferenceBaseUrl(baseUrl)) || (routeId === 'merge-gateway' && - !isCanonicalMergeGatewayInferenceBaseUrl(baseUrl)) + !isCanonicalMergeGatewayInferenceBaseUrl(baseUrl)) || + (routeId === 'requesty' && + !isCanonicalRequestyInferenceBaseUrl(baseUrl)) ) { return undefined } diff --git a/src/integrations/routeMetadata.test.ts b/src/integrations/routeMetadata.test.ts index 230b74c1d..c9b85c0ee 100644 --- a/src/integrations/routeMetadata.test.ts +++ b/src/integrations/routeMetadata.test.ts @@ -555,6 +555,34 @@ test('Requesty regional hosts resolve to the requesty route', () => { expect( resolveRouteIdFromBaseUrl('https://router.requesty.ai.evil.example/v1'), ).toBe(null) + // Requesty route identity follows the same boundary as its credential. + for (const baseUrl of [ + 'http://router.requesty.ai/v1', + 'https://router.requesty.ai:8443/v1', + 'https://router.requesty.ai/v1?x=1', + 'https://router.requesty.ai/v1/models', + 'https://router.eu.requesty.ai/v2', + ]) { + expect(resolveRouteIdFromBaseUrl(baseUrl)).toBe(null) + } + expect( + resolveActiveRouteIdFromEnv( + { CLAUDE_CODE_USE_OPENAI: '1' }, + { + activeProfileProvider: 'requesty', + activeProfileBaseUrl: 'https://proxy.example/v1', + }, + ), + ).not.toBe('requesty') + expect( + resolveActiveRouteIdFromEnv( + { CLAUDE_CODE_USE_OPENAI: '1' }, + { + activeProfileProvider: 'requesty', + activeProfileBaseUrl: 'https://router.requesty.ai/v1', + }, + ), + ).toBe('requesty') }) test('Requesty credential is limited to the canonical inference base URLs', () => { diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index c21ff4d09..dffc62452 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -1553,6 +1553,12 @@ export function resolveRouteIdFromBaseUrl( ) { continue } + if ( + route.id === 'requesty' && + !isCanonicalRequestyInferenceBaseUrl(baseUrl) + ) { + continue + } return route.id } } @@ -1577,7 +1583,9 @@ export function resolveRouteIdFromBaseUrl( (route.id === 'commandcode' && !isCanonicalCommandcodeInferenceBaseUrl(baseUrl)) || (route.id === 'api-route' && - !isApiRouteBaseUrl(baseUrl)) + !isApiRouteBaseUrl(baseUrl)) || + (route.id === 'requesty' && + !isCanonicalRequestyInferenceBaseUrl(baseUrl)) ) { continue } @@ -1629,6 +1637,9 @@ function profileRouteHonorsBaseUrlBoundary( if (routeId === 'api-route') { return isCanonicalApiRouteInferenceBaseUrl(baseUrl) } + if (routeId === 'requesty') { + return isCanonicalRequestyInferenceBaseUrl(baseUrl) + } if (routeId === 'zai') { return !baseUrl || isCanonicalZaiCodingPlanBaseUrl(baseUrl) } diff --git a/src/utils/managedEnv.test.ts b/src/utils/managedEnv.test.ts index 35b046a33..a91c9403d 100644 --- a/src/utils/managedEnv.test.ts +++ b/src/utils/managedEnv.test.ts @@ -46,6 +46,7 @@ const ENV_KEYS = [ 'COMMAND_CODE_API_KEY', 'API_ROUTE_API_KEY', 'API_ROUTE_MODEL', + 'REQUESTY_API_KEY', 'OPENAI_API_KEY', 'OPENAI_AZURE_STYLE', 'OPENAI_BASE_URL', @@ -147,6 +148,21 @@ describe('host-managed API Route settings boundary', () => { }) describe('applyConfigEnvironmentVariables', () => { + it('preserves a host-managed Requesty key against settings env', () => { + process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST = '1' + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://router.requesty.ai/v1' + process.env.REQUESTY_API_KEY = 'host-requesty-key' + saveGlobalConfig(current => ({ + ...current, + env: { REQUESTY_API_KEY: 'settings-requesty-key' }, + })) + + applyConfigEnvironmentVariables() + + expect(process.env.REQUESTY_API_KEY).toBe('host-requesty-key') + }) + it('preserves the complete host-managed Command Code route against settings env', () => { process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST = '1' process.env.CLAUDE_CODE_USE_OPENAI = '1' diff --git a/src/utils/managedEnvConstants.ts b/src/utils/managedEnvConstants.ts index e5b8eb735..13f551227 100644 --- a/src/utils/managedEnvConstants.ts +++ b/src/utils/managedEnvConstants.ts @@ -51,6 +51,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([ 'COMMAND_CODE_API_KEY', 'API_ROUTE_API_KEY', 'MERGE_GATEWAY_API_KEY', + 'REQUESTY_API_KEY', 'ORCAROUTER_API_KEY', 'RUNCRATE_API_KEY', 'OPENAI_API_KEY', diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index c25b216dd..64d73d63d 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -620,6 +620,39 @@ describe('applyProviderProfileToProcessEnv', () => { expect(startupEnv.API_ROUTE_API_KEY).toBeUndefined() }, 20_000) + test('retargeted Requesty profile runs generically and skips discovery', async () => { + const fetchMock = mock(() => Promise.resolve(new Response('{}'))) + globalThis.fetch = fetchMock as unknown as typeof fetch + const { resolveProfileCapabilityRouteId, setActiveProviderProfile } = + await importFreshProviderProfileModules() + const profile = buildRequestyProfile({ + id: 'requesty_proxy', + baseUrl: 'https://proxy.example/v1', + }) + saveMockGlobalConfig(current => ({ + ...current, + providerProfiles: [profile], + })) + + setActiveProviderProfile(profile.id, { configDir: testConfigDir ?? undefined }) + await Promise.resolve() + + expect(process.env.OPENAI_API_KEY).toBeUndefined() + expect(process.env.REQUESTY_API_KEY).toBeUndefined() + expect(fetchMock).not.toHaveBeenCalled() + + for (const baseUrl of [ + 'https://proxy.example/v1', + 'http://router.requesty.ai/v1', + 'https://router.requesty.ai/v1/models', + ]) { + expect(resolveProfileCapabilityRouteId('requesty', baseUrl)).toBe('custom') + } + expect( + resolveProfileCapabilityRouteId('requesty', 'https://router.eu.requesty.ai/v1'), + ).toBe('requesty') + }, 20_000) + test('keyless canonical LLMTR profile adopts its ambient dedicated key', async () => { const { applyProviderProfileToProcessEnv } = await importFreshProviderProfileModules() @@ -5365,6 +5398,67 @@ describe('setActiveProviderProfile', () => { } }) + test('a saved Requesty proxy profile keeps its auth off a shell retarget onto canonical Requesty', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-')) + const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-')) + process.chdir(tempDir) + process.env.CLAUDE_CONFIG_DIR = configDir + + try { + const { setActiveProviderProfile } = + await importFreshProviderProfileModules() + const proxy = buildRequestyProfile({ + id: 'requesty_proxy', + baseUrl: 'https://requesty-proxy.example/v1', + apiKey: 'saved-proxy-key', + authHeader: 'X-Proxy-Auth', + authScheme: 'raw', + authHeaderValue: 'saved-proxy-secret', + customHeaders: { 'X-Proxy-Trace': 'saved-proxy-header' }, + }) + saveMockGlobalConfig(current => ({ + ...current, + providerProfiles: [proxy], + })) + + setActiveProviderProfile('requesty_proxy', { configDir }) + const persisted = JSON.parse( + readFileSync(join(configDir, '.openclaude-profile.json'), 'utf8'), + ) + expect(persisted.env).toMatchObject({ + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'requesty', + OPENAI_BASE_URL: 'https://requesty-proxy.example/v1', + OPENAI_AUTH_HEADER_VALUE: 'saved-proxy-secret', + }) + expect(persisted.env.OPENAI_API_KEY).toBeUndefined() + + const { buildStartupEnvFromProfile } = await import( + `./providerProfile.js?ts=${Date.now()}-${Math.random()}` + ) + const ownUrlEnv = await buildStartupEnvFromProfile({ + persisted, + processEnv: {}, + }) + expect(ownUrlEnv.OPENAI_AUTH_HEADER_VALUE).toBe('saved-proxy-secret') + + const retargetedEnv = await buildStartupEnvFromProfile({ + persisted, + processEnv: { OPENAI_BASE_URL: 'https://router.requesty.ai/v1' }, + }) + expect(retargetedEnv.OPENAI_BASE_URL).toBe('https://router.requesty.ai/v1') + expect(retargetedEnv.OPENAI_API_KEY).toBeUndefined() + expect(retargetedEnv.REQUESTY_API_KEY).toBeUndefined() + expect(retargetedEnv.OPENAI_AUTH_HEADER).toBeUndefined() + expect(retargetedEnv.OPENAI_AUTH_SCHEME).toBeUndefined() + expect(retargetedEnv.OPENAI_AUTH_HEADER_VALUE).toBeUndefined() + expect(retargetedEnv.ANTHROPIC_CUSTOM_HEADERS).toBeUndefined() + } finally { + process.chdir(originalCwd) + rmSync(tempDir, { recursive: true, force: true }) + rmSync(configDir, { recursive: true, force: true }) + } + }) + test('keyed canonical LLMTR profiles persist their saved dedicated credential across restart', async () => { const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-')) const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-')) diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 134276be2..cebe5daaa 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -377,7 +377,8 @@ export function resolveProfileCapabilityRouteId( providerRouteId === 'api-route' || providerRouteId === 'llmtr' || providerRouteId === 'merge-gateway' || - providerRouteId === 'commandcode') && + providerRouteId === 'commandcode' || + providerRouteId === 'requesty') && baseUrl && !(providerRouteId === 'cloudflare' ? isCloudflareBaseUrl(baseUrl) @@ -391,6 +392,8 @@ export function resolveProfileCapabilityRouteId( ? isCanonicalCommandcodeInferenceBaseUrl(baseUrl) : providerRouteId === 'merge-gateway' ? isCanonicalMergeGatewayInferenceBaseUrl(baseUrl) + : providerRouteId === 'requesty' + ? isCanonicalRequestyInferenceBaseUrl(baseUrl) : isCanonicalLlmtrInferenceBaseUrl(baseUrl)) ) { return 'custom' @@ -2103,6 +2106,11 @@ function buildOpenAICompatibleStartupEnv( if (activeProfileRouteId === 'commandcode') { env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'commandcode' } + // Relaunch uses this identity to keep a saved non-canonical Requesty + // profile's custom auth and headers off a shell retarget to Requesty. + if (activeProfileRouteId === 'requesty') { + env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'requesty' + } if ( activeProfile.apiKey && !withholdRetargetedApismartCredential && @@ -2439,6 +2447,9 @@ function triggerStartupDiscoveryRefreshForProfile( if (route.routeId === 'commandcode' && !isCommandcodeProfile(profile)) { return } + if (route.routeId === 'requesty' && !isRequestyProfile(profile)) { + return + } void refreshStartupDiscoveryForRoute(route.routeId, { baseUrl: profile.baseUrl, diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index 4823933bc..f3ff4fd92 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -44,6 +44,7 @@ const ENV_KEYS = [ 'LONGCAT_API_KEY', 'LLMTR_API_KEY', 'MERGE_GATEWAY_API_KEY', + 'REQUESTY_API_KEY', 'CMD_API_KEY', 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', @@ -596,6 +597,27 @@ test('noncanonical ApiSmart paths do not validate a dedicated credential', async ) }) +test('noncanonical Requesty host paths do not validate a dedicated credential', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_MODEL = 'openai/gpt-5-mini' + process.env.REQUESTY_API_KEY = 'requesty-key' + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_API_KEYS + + process.env.OPENAI_BASE_URL = 'https://router.requesty.ai/v1' + await expect(getProviderValidationError(process.env)).resolves.toBeNull() + + // The request withholds REQUESTY_API_KEY here, so validation must not accept it. + process.env.OPENAI_BASE_URL = 'https://router.requesty.ai/v1/models' + const message = await getProviderValidationError(process.env) + expect(message).toContain( + 'OPENAI_API_KEYS or OPENAI_API_KEY is required when CLAUDE_CODE_USE_OPENAI=1', + ) + + process.env.OPENAI_API_KEY = 'generic-proxy-key' + await expect(getProviderValidationError(process.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 d776b9848..0af3d4f5a 100644 --- a/src/utils/providerValidation.ts +++ b/src/utils/providerValidation.ts @@ -23,6 +23,7 @@ import { isCanonicalConcentrateInferenceBaseUrl, isCanonicalCommandcodeInferenceBaseUrl, isCanonicalMergeGatewayInferenceBaseUrl, + isCanonicalRequestyInferenceBaseUrl, isCloudflareBaseUrl, isLongcatBaseUrl, matchHostnameAgainstRouteHosts, @@ -326,7 +327,9 @@ function getRuntimeValidationTarget( (target.descriptor.id === 'merge-gateway' && !isCanonicalMergeGatewayInferenceBaseUrl(request.baseUrl)) || (target.descriptor.id === 'commandcode' && - !isCanonicalCommandcodeInferenceBaseUrl(request.baseUrl))) + !isCanonicalCommandcodeInferenceBaseUrl(request.baseUrl)) || + (target.descriptor.id === 'requesty' && + !isCanonicalRequestyInferenceBaseUrl(request.baseUrl))) ) { return false }