From cb0e7311cc2eef44ea27e4af3c9f88f54de1f09e Mon Sep 17 00:00:00 2001 From: Paarth Ahuja Date: Mon, 8 Dec 2025 16:16:37 -0500 Subject: [PATCH] Prevent orphaned tool outputs from triggering Codex 400s --- lib/request/request-transformer.ts | 9 ++++--- package-lock.json | 4 +-- test/request-transformer.test.ts | 43 ++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/request/request-transformer.ts b/lib/request/request-transformer.ts index 9094936..de2cae2 100644 --- a/lib/request/request-transformer.ts +++ b/lib/request/request-transformer.ts @@ -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 @@ -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; }); diff --git a/package-lock.json b/package-lock.json index ccdcd2f..2d95871 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opencode-openai-codex-auth", - "version": "4.0.0", + "version": "4.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opencode-openai-codex-auth", - "version": "4.0.0", + "version": "4.0.3", "license": "MIT", "dependencies": { "@openauthjs/openauth": "^0.4.3", diff --git a/test/request-transformer.test.ts b/test/request-transformer.test.ts index 025d6ab..db11ac5 100644 --- a/test/request-transformer.test.ts +++ b/test/request-transformer.test.ts @@ -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 = {