Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions lib/request/request-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,8 @@ export async function transformRequestBody(
}

// Filter orphaned function_call_output items (where function_call was an item_reference that got filtered)
// Keep matched pairs for compaction context
if (!body.tools && body.input) {
// Keep matched pairs for compaction context but drop unmatched outputs to avoid Codex API 400 errors
if (body.input) {
// Collect all call_ids from function_call items
const functionCallIds = new Set(
body.input
Expand All @@ -454,7 +454,10 @@ export async function transformRequestBody(
// Only filter function_call_output items that don't have a matching function_call
body.input = body.input.filter((item) => {
if (item.type === "function_call_output") {
return functionCallIds.has(item.call_id);
return (
typeof item.call_id === "string" &&
functionCallIds.has(item.call_id)
);
}
return true;
});
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions test/request-transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,49 @@ describe('Request Transformer Module', () => {
expect(result.input![2].type).toBe('function_call_output');
});

it('should drop orphaned function_call_output even when tools are present', async () => {
const body: RequestBody = {
model: 'gpt-5-codex',
input: [
{ type: 'message', role: 'user', content: 'hello' },
{ type: 'function_call_output', role: 'assistant', call_id: 'call_ghost', output: '{}' } as any,
],
tools: [{ name: 'task' }],
};

const result = await transformRequestBody(body, codexInstructions);

expect(result.input).toBeDefined();
const outputs = result.input!.filter(
(item) => item.type === 'function_call_output',
);
expect(outputs).toHaveLength(0);
});

it('should keep matched function_call pairs when tools are present', async () => {
const body: RequestBody = {
model: 'gpt-5-codex',
input: [
{ type: 'message', role: 'user', content: 'hello' },
{ type: 'function_call', call_id: 'call_keep', name: 'task', arguments: '{}' } as any,
{ type: 'function_call_output', call_id: 'call_keep', output: 'success' } as any,
],
tools: [{ name: 'task' }],
};

const result = await transformRequestBody(body, codexInstructions);

expect(result.input).toBeDefined();
const outputs = result.input!.filter(
(item) => item.type === 'function_call_output',
);
expect(outputs).toHaveLength(1);
const calls = result.input!.filter(
(item) => item.type === 'function_call' && item.call_id === 'call_keep',
);
expect(calls).toHaveLength(1);
});

describe('CODEX_MODE parameter', () => {
it('should use bridge message when codexMode=true and tools present (default)', async () => {
const body: RequestBody = {
Expand Down