fix(proxy): shape direct OpenAI reasoning chat payloads

This commit is contained in:
Tam Nhu Tran
2026-04-30 12:33:02 -04:00
parent 65e10b3473
commit d9ace607e5
3 changed files with 116 additions and 7 deletions
+49 -7
View File
@@ -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<string
};
}
function isDirectOpenAIReasoningChatModel(
profile: OpenAICompatProfileConfig,
model: string | undefined
): boolean {
return (
profile.provider === 'openai' &&
typeof model === 'string' &&
DIRECT_OPENAI_REASONING_CHAT_MODEL.test(model.trim().toLowerCase())
);
}
function shapeUpstreamChatPayload(
payload: ProxyOpenAIRequest,
profile: OpenAICompatProfileConfig
): ProxyOpenAIRequest {
if (!isDirectOpenAIReasoningChatModel(profile, payload.model)) {
return payload;
}
const shaped = { ...payload };
if (shaped.max_tokens !== undefined) {
shaped.max_completion_tokens = shaped.max_tokens;
delete shaped.max_tokens;
}
delete shaped.metadata;
if ((shaped.tools?.length ?? 0) > 0) {
delete shaped.reasoning_effort;
}
return shaped;
}
function buildUpstreamRequest(
profile: OpenAICompatProfileConfig,
rawBody: unknown
): { body: string; route: ReturnType<typeof resolveProxyRequestRoute> } {
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 };
}
@@ -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[];
@@ -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);
});
});