diff --git a/src/server/routes/proxy/upstreamEndpoint.test.ts b/src/server/routes/proxy/upstreamEndpoint.test.ts index c5ad01f..358a2d8 100644 --- a/src/server/routes/proxy/upstreamEndpoint.test.ts +++ b/src/server/routes/proxy/upstreamEndpoint.test.ts @@ -696,6 +696,93 @@ describe('buildUpstreamEndpointRequest', () => { ]); }); + it('applies a sub2api-style allowlist to generic passthrough headers', () => { + const request = buildUpstreamEndpointRequest({ + endpoint: 'chat', + modelName: 'upstream-gpt', + stream: false, + tokenValue: 'sk-test', + sitePlatform: 'sub2api', + siteUrl: 'https://example.com', + openaiBody: { + model: 'gpt-5.2', + messages: [{ role: 'user', content: 'hello' }], + }, + downstreamFormat: 'openai', + downstreamHeaders: { + accept: 'application/json', + 'accept-language': 'zh-CN', + 'user-agent': 'client-ua/1.0', + originator: 'codex_cli_rs', + session_id: 'session-123', + conversation_id: 'conversation-123', + 'x-codex-turn-state': 'turn-state', + 'x-codex-turn-metadata': 'turn-metadata', + origin: 'https://client.example', + referer: 'https://client.example/chat', + 'x-forwarded-for': '203.0.113.1', + 'x-real-ip': '203.0.113.2', + version: '0.202.0', + 'x-test-header': 'drop-me', + }, + }); + + expect(request.headers.accept).toBe('application/json'); + expect(request.headers['accept-language']).toBe('zh-CN'); + expect(request.headers['user-agent']).toBe('client-ua/1.0'); + expect(request.headers.originator).toBe('codex_cli_rs'); + expect(request.headers.session_id).toBe('session-123'); + expect(request.headers.conversation_id).toBe('conversation-123'); + expect(request.headers['x-codex-turn-state']).toBe('turn-state'); + expect(request.headers['x-codex-turn-metadata']).toBe('turn-metadata'); + + expect(request.headers.origin).toBeUndefined(); + expect(request.headers.referer).toBeUndefined(); + expect(request.headers['x-forwarded-for']).toBeUndefined(); + expect(request.headers['x-real-ip']).toBeUndefined(); + expect(request.headers.version).toBeUndefined(); + expect(request.headers['x-test-header']).toBeUndefined(); + }); + + it('preserves codex compatibility headers while stripping browser and ip passthrough headers', () => { + const request = buildUpstreamEndpointRequest({ + endpoint: 'responses', + modelName: 'gpt-5.2-codex', + stream: false, + tokenValue: 'oauth-access-token', + oauthProvider: 'codex', + sitePlatform: 'codex', + siteUrl: 'https://chatgpt.com/backend-api/codex', + openaiBody: { + model: 'gpt-5.2-codex', + input: 'hello codex', + }, + downstreamFormat: 'openai', + downstreamHeaders: { + 'user-agent': 'OpenClaw/1.0', + version: '0.202.0', + session_id: 'session-from-client', + 'x-responsesapi-include-timing-metrics': '1', + origin: 'https://openclaw.example', + referer: 'https://openclaw.example/app', + 'x-forwarded-for': '203.0.113.1', + 'x-real-ip': '203.0.113.2', + }, + providerHeaders: { + Originator: 'codex_cli_rs', + }, + } as any); + + expect(request.headers.Version).toBe('0.202.0'); + expect(request.headers.Session_id).toBe('session-from-client'); + expect(request.headers['User-Agent']).toBe('OpenClaw/1.0'); + expect(request.headers['x-responsesapi-include-timing-metrics']).toBe('1'); + expect(request.headers.origin).toBeUndefined(); + expect(request.headers.referer).toBeUndefined(); + expect(request.headers['x-forwarded-for']).toBeUndefined(); + expect(request.headers['x-real-ip']).toBeUndefined(); + }); + it('builds codex responses requests against backend-api path and preserves oauth provider headers', () => { const request = buildUpstreamEndpointRequest({ endpoint: 'responses', diff --git a/src/server/routes/proxy/upstreamEndpoint.ts b/src/server/routes/proxy/upstreamEndpoint.ts index 27df7d2..18ca9ed 100644 --- a/src/server/routes/proxy/upstreamEndpoint.ts +++ b/src/server/routes/proxy/upstreamEndpoint.ts @@ -115,6 +115,20 @@ const BLOCKED_PASSTHROUGH_HEADERS = new Set([ 'sec-websocket-version', 'sec-websocket-extensions', ]); +const GENERIC_PASSTHROUGH_ALLOWED_HEADERS = new Set([ + 'accept', + 'accept-language', + 'conversation-id', + 'conversation_id', + 'openai-beta', + 'originator', + 'session-id', + 'session_id', + 'user-agent', + 'x-codex-beta-features', + 'x-codex-turn-metadata', + 'x-codex-turn-state', +]); const METAPI_INTERNAL_HEADER_BLOCKLIST = new Set([ 'x-metapi-tester-request', 'x-metapi-tester-forced-channel-id', @@ -128,6 +142,7 @@ function shouldSkipPassthroughHeader(key: string): boolean { if (HOP_BY_HOP_HEADERS.has(key) || BLOCKED_PASSTHROUGH_HEADERS.has(key)) return true; if (METAPI_INTERNAL_HEADER_BLOCKLIST.has(key)) return true; if (key.startsWith('x-metapi-')) return true; + if (!GENERIC_PASSTHROUGH_ALLOWED_HEADERS.has(key)) return true; return false; } @@ -197,6 +212,28 @@ function extractResponsesPassthroughHeaders( return forwarded; } +function extractCodexPassthroughHeaders( + headers?: Record, +): Record { + if (!headers) return {}; + + const forwarded: Record = {}; + for (const [rawKey, rawValue] of Object.entries(headers)) { + const key = rawKey.toLowerCase(); + const shouldForward = ( + key === 'version' + || key === 'x-responsesapi-include-timing-metrics' + ); + if (!shouldForward) continue; + + const value = headerValueToString(rawValue); + if (!value) continue; + forwarded[key] = value; + } + + return forwarded; +} + function extractClaudeBetasFromBody(body: Record): { body: Record; betas: string[]; @@ -725,8 +762,12 @@ export function buildUpstreamEndpointRequest(input: { }; const passthroughHeaders = extractSafePassthroughHeaders(input.downstreamHeaders); + const codexPassthroughHeaders = sitePlatform === 'codex' + ? extractCodexPassthroughHeaders(input.downstreamHeaders) + : {}; const commonHeaders: Record = { ...passthroughHeaders, + ...codexPassthroughHeaders, 'Content-Type': 'application/json', ...(input.providerHeaders || {}), };