forked from boos/BoosAPI
Merge pull request #150 from cita-777/fix/codex-responses-runtime-parity
fix: harden codex runtime header forwarding
This commit is contained in:
@@ -994,7 +994,7 @@ describe('oauth routes', { timeout: 15_000 }, () => {
|
|||||||
success: true,
|
success: true,
|
||||||
quota: expect.objectContaining({
|
quota: expect.objectContaining({
|
||||||
status: 'unsupported',
|
status: 'unsupported',
|
||||||
providerMessage: 'current local CLIProxyAPI evidence does not expose official quota windows for antigravity oauth',
|
providerMessage: 'official quota windows are not exposed for antigravity oauth',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -173,6 +173,9 @@ describe('responses proxy codex oauth refresh', () => {
|
|||||||
const response = await app.inject({
|
const response = await app.inject({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/v1/responses',
|
url: '/v1/responses',
|
||||||
|
headers: {
|
||||||
|
'user-agent': 'CodexClient/1.0',
|
||||||
|
},
|
||||||
payload: {
|
payload: {
|
||||||
model: 'gpt-5.2-codex',
|
model: 'gpt-5.2-codex',
|
||||||
input: 'hello codex',
|
input: 'hello codex',
|
||||||
@@ -194,7 +197,7 @@ describe('responses proxy codex oauth refresh', () => {
|
|||||||
expect(secondOptions.headers.Version || secondOptions.headers.version).toBe('0.101.0');
|
expect(secondOptions.headers.Version || secondOptions.headers.version).toBe('0.101.0');
|
||||||
expect(String(secondOptions.headers.Session_id || secondOptions.headers.session_id || '')).toMatch(/^[0-9a-f-]{36}$/i);
|
expect(String(secondOptions.headers.Session_id || secondOptions.headers.session_id || '')).toMatch(/^[0-9a-f-]{36}$/i);
|
||||||
expect(secondOptions.headers.Conversation_id || secondOptions.headers.conversation_id).toBeUndefined();
|
expect(secondOptions.headers.Conversation_id || secondOptions.headers.conversation_id).toBeUndefined();
|
||||||
expect(secondOptions.headers['User-Agent'] || secondOptions.headers['user-agent']).toBe('codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464');
|
expect(secondOptions.headers['User-Agent'] || secondOptions.headers['user-agent']).toBe('CodexClient/1.0');
|
||||||
expect(secondOptions.headers.Accept || secondOptions.headers.accept).toBe('text/event-stream');
|
expect(secondOptions.headers.Accept || secondOptions.headers.accept).toBe('text/event-stream');
|
||||||
expect(secondOptions.headers.Connection || secondOptions.headers.connection).toBe('Keep-Alive');
|
expect(secondOptions.headers.Connection || secondOptions.headers.connection).toBe('Keep-Alive');
|
||||||
expect(response.json()?.output_text).toContain('ok after codex token refresh');
|
expect(response.json()?.output_text).toContain('ok after codex token refresh');
|
||||||
@@ -272,6 +275,50 @@ describe('responses proxy codex oauth refresh', () => {
|
|||||||
expect(forwardedBody.prompt_cache_key).toBe('codex-cache-123');
|
expect(forwardedBody.prompt_cache_key).toBe('codex-cache-123');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('strips generic downstream headers before forwarding codex responses upstream', async () => {
|
||||||
|
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
||||||
|
id: 'resp_codex_header_filter',
|
||||||
|
object: 'response',
|
||||||
|
model: 'gpt-5.2-codex',
|
||||||
|
status: 'completed',
|
||||||
|
output_text: 'ok with filtered headers',
|
||||||
|
usage: { input_tokens: 4, output_tokens: 2, total_tokens: 6 },
|
||||||
|
}), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/v1/responses',
|
||||||
|
headers: {
|
||||||
|
'openai-beta': 'responses-2025-03-11',
|
||||||
|
'x-openai-client-user-agent': '{"client":"openclaw"}',
|
||||||
|
origin: 'https://openclaw.example',
|
||||||
|
referer: 'https://openclaw.example/app',
|
||||||
|
'user-agent': 'OpenClaw/1.0',
|
||||||
|
version: '0.202.0',
|
||||||
|
session_id: 'session-from-client',
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
model: 'gpt-5.2-codex',
|
||||||
|
input: 'hello codex',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const [, options] = fetchMock.mock.calls[0] as [string, any];
|
||||||
|
expect(options.headers.Version || options.headers.version).toBe('0.202.0');
|
||||||
|
expect(options.headers.Session_id || options.headers.session_id).toBe('session-from-client');
|
||||||
|
expect(options.headers['User-Agent'] || options.headers['user-agent']).toBe('OpenClaw/1.0');
|
||||||
|
expect(options.headers['openai-beta']).toBeUndefined();
|
||||||
|
expect(options.headers['x-openai-client-user-agent']).toBeUndefined();
|
||||||
|
expect(options.headers.origin).toBeUndefined();
|
||||||
|
expect(options.headers.referer).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('records codex usage_limit_reached reset hints on upstream 429 failures', async () => {
|
it('records codex usage_limit_reached reset hints on upstream 429 failures', async () => {
|
||||||
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
fetchMock.mockResolvedValue(new Response(JSON.stringify({
|
||||||
error: {
|
error: {
|
||||||
|
|||||||
@@ -286,8 +286,15 @@ function buildCodexRuntimeHeaders(input: {
|
|||||||
explicitSessionId?: string | null;
|
explicitSessionId?: string | null;
|
||||||
continuityKey?: string | null;
|
continuityKey?: string | null;
|
||||||
}): Record<string, string> {
|
}): Record<string, string> {
|
||||||
|
const authorization = (
|
||||||
|
getInputHeader(input.baseHeaders, 'authorization')
|
||||||
|
|| getInputHeader(input.baseHeaders, 'Authorization')
|
||||||
|
|| ''
|
||||||
|
);
|
||||||
const originator = getInputHeader(input.providerHeaders, 'originator') || 'codex_cli_rs';
|
const originator = getInputHeader(input.providerHeaders, 'originator') || 'codex_cli_rs';
|
||||||
const accountId = getInputHeader(input.providerHeaders, 'chatgpt-account-id');
|
const accountId = getInputHeader(input.providerHeaders, 'chatgpt-account-id');
|
||||||
|
const version = getInputHeader(input.baseHeaders, 'version') || CODEX_CLIENT_VERSION;
|
||||||
|
const userAgent = getInputHeader(input.baseHeaders, 'user-agent') || CODEX_DEFAULT_USER_AGENT;
|
||||||
const explicitSessionId = asTrimmedString(input.explicitSessionId);
|
const explicitSessionId = asTrimmedString(input.explicitSessionId);
|
||||||
const continuityKey = asTrimmedString(input.continuityKey);
|
const continuityKey = asTrimmedString(input.continuityKey);
|
||||||
const sessionId = (
|
const sessionId = (
|
||||||
@@ -305,13 +312,14 @@ function buildCodexRuntimeHeaders(input: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...input.baseHeaders,
|
Authorization: authorization,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
...(accountId ? { 'Chatgpt-Account-Id': accountId } : {}),
|
...(accountId ? { 'Chatgpt-Account-Id': accountId } : {}),
|
||||||
Originator: originator,
|
Originator: originator,
|
||||||
Version: CODEX_CLIENT_VERSION,
|
Version: version,
|
||||||
Session_id: sessionId,
|
Session_id: sessionId,
|
||||||
...(conversationId ? { Conversation_id: conversationId } : {}),
|
...(conversationId ? { Conversation_id: conversationId } : {}),
|
||||||
'User-Agent': CODEX_DEFAULT_USER_AGENT,
|
'User-Agent': userAgent,
|
||||||
Accept: 'text/event-stream',
|
Accept: 'text/event-stream',
|
||||||
Connection: 'Keep-Alive',
|
Connection: 'Keep-Alive',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ describe('oauth quota snapshot helpers', () => {
|
|||||||
expect(snapshot).toEqual({
|
expect(snapshot).toEqual({
|
||||||
status: 'unsupported',
|
status: 'unsupported',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
providerMessage: 'current local CLIProxyAPI evidence does not expose official quota windows for antigravity oauth',
|
providerMessage: 'official quota windows are not exposed for antigravity oauth',
|
||||||
windows: {
|
windows: {
|
||||||
fiveHour: {
|
fiveHour: {
|
||||||
supported: false,
|
supported: false,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function buildProviderUnsupportedSnapshot(provider: string): OauthQuotaSnapshot
|
|||||||
return {
|
return {
|
||||||
status: 'unsupported',
|
status: 'unsupported',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
providerMessage: `current local CLIProxyAPI evidence does not expose official quota windows for ${provider} oauth`,
|
providerMessage: `official quota windows are not exposed for ${provider} oauth`,
|
||||||
windows: {
|
windows: {
|
||||||
fiveHour: buildUnsupportedWindow('official 5h quota window is unavailable for this provider'),
|
fiveHour: buildUnsupportedWindow('official 5h quota window is unavailable for this provider'),
|
||||||
sevenDay: buildUnsupportedWindow('official 7d quota window is unavailable for this provider'),
|
sevenDay: buildUnsupportedWindow('official 7d quota window is unavailable for this provider'),
|
||||||
|
|||||||
Reference in New Issue
Block a user