From d9ace607e58caafd71224e02ed9f297b7dcacecb Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 12:33:02 -0400 Subject: [PATCH] fix(proxy): shape direct OpenAI reasoning chat payloads --- src/proxy/server/messages-route.ts | 56 ++++++++++++++-- src/proxy/transformers/request-transformer.ts | 1 + .../integration/proxy/request-routing.test.ts | 66 +++++++++++++++++++ 3 files changed, 116 insertions(+), 7 deletions(-) diff --git a/src/proxy/server/messages-route.ts b/src/proxy/server/messages-route.ts index 965e67a3..05cf485e 100644 --- a/src/proxy/server/messages-route.ts +++ b/src/proxy/server/messages-route.ts @@ -2,13 +2,17 @@ import * as http from 'http'; import type { Dispatcher } from 'undici'; import type { OpenAICompatProfileConfig } from '../profile-router'; import { resolveProxyRequestRoute } from '../request-router'; -import { ProxyRequestTransformer } from '../transformers/request-transformer'; +import { + ProxyRequestTransformer, + type ProxyOpenAIRequest, +} from '../transformers/request-transformer'; import { ProxySseStreamTransformer } from '../transformers/sse-stream-transformer'; import { resolveOpenAIChatCompletionsUrl } from '../upstream-url'; import { createLogger } from '../../services/logging'; import { pipeWebResponseToNode, readJsonBody, writeJson } from './http-helpers'; const REQUEST_TIMEOUT_MS = 600_000; +const DIRECT_OPENAI_REASONING_CHAT_MODEL = /^(?:gpt-5|o[134])(?:[-.]|$)/; const logger = createLogger('proxy:openai-compat:messages'); class ProxyInputError extends Error { @@ -26,11 +30,46 @@ function buildUpstreamHeaders(profile: OpenAICompatProfileConfig): Record 0) { + delete shaped.reasoning_effort; + } + + return shaped; +} + function buildUpstreamRequest( profile: OpenAICompatProfileConfig, rawBody: unknown ): { body: string; route: ReturnType } { - let transformed; + let transformed: ProxyOpenAIRequest; try { const transformer = new ProxyRequestTransformer(); transformed = transformer.transform(rawBody); @@ -39,11 +78,14 @@ function buildUpstreamRequest( throw new ProxyInputError(message); } const route = resolveProxyRequestRoute(profile, transformed); - const body = { - ...transformed, - model: route.model || route.profile.model, - stream: transformed.stream === true, - }; + const body = shapeUpstreamChatPayload( + { + ...transformed, + model: route.model || route.profile.model, + stream: transformed.stream === true, + }, + route.profile + ); return { body: JSON.stringify(body), route }; } diff --git a/src/proxy/transformers/request-transformer.ts b/src/proxy/transformers/request-transformer.ts index 0942244e..f1f9346f 100644 --- a/src/proxy/transformers/request-transformer.ts +++ b/src/proxy/transformers/request-transformer.ts @@ -127,6 +127,7 @@ export interface ProxyOpenAIRequest { parallel_tool_calls?: boolean; messages: OpenAIMessage[]; max_tokens?: number; + max_completion_tokens?: number; temperature?: number; top_p?: number; stop?: string[]; diff --git a/tests/integration/proxy/request-routing.test.ts b/tests/integration/proxy/request-routing.test.ts index 7e44ae13..3fe350bf 100644 --- a/tests/integration/proxy/request-routing.test.ts +++ b/tests/integration/proxy/request-routing.test.ts @@ -353,4 +353,70 @@ describe('openai proxy request routing', () => { }); expect((bodies[0]?.body as { reasoning?: unknown } | undefined)?.reasoning).toBeUndefined(); }); + + it('shapes direct OpenAI reasoning-model chat payloads after route resolution', async () => { + const hits: string[] = []; + const bodies: Array<{ label: string; body: unknown }> = []; + const upstreamPort = await startMockUpstream('openai', hits, bodies); + + const settingsPath = writeSettings('openai', { + ANTHROPIC_BASE_URL: 'https://api.openai.com/v1', + ANTHROPIC_AUTH_TOKEN: 'openai_token', + ANTHROPIC_MODEL: 'gpt-5.4', + }); + + fs.writeFileSync( + path.join(tempDir, '.ccs', 'config.json'), + JSON.stringify({ profiles: { openai: settingsPath } }, null, 2), + 'utf8' + ); + + const profile: OpenAICompatProfileConfig = { + profileName: 'openai', + settingsPath, + baseUrl: `http://127.0.0.1:${upstreamPort}`, + apiKey: 'openai_token', + provider: 'openai', + model: 'gpt-5.4', + }; + proxyServer = startOpenAICompatProxyServer({ + profile, + port: 0, + authToken: 'test-proxy-token', + }); + proxyPort = await waitForServerListening(proxyServer); + + const response = await requestProxy({ + model: 'gpt-5.4', + thinking: { type: 'adaptive' }, + output_config: { effort: 'max' }, + max_tokens: 1024, + metadata: { trace: 'abc' }, + tools: [{ name: 'search', description: 'Search docs', input_schema: { type: 'object' } }], + messages: [{ role: 'user', content: 'think with tools' }], + }); + + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + content: [{ type: 'text', text: 'Reply from openai' }], + }); + expect(hits).toEqual(['openai']); + + const body = bodies[0]?.body as { + max_tokens?: number; + max_completion_tokens?: number; + metadata?: unknown; + reasoning_effort?: string; + tools?: unknown[]; + }; + expect(body).toMatchObject({ + model: 'gpt-5.4', + max_completion_tokens: 1024, + tool_choice: 'auto', + }); + expect(body.max_tokens).toBeUndefined(); + expect(body.metadata).toBeUndefined(); + expect(body.reasoning_effort).toBeUndefined(); + expect(body.tools?.length).toBe(1); + }); });