From 27a0c54cee87dceff4546e97fe5242bb5940aeff Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 23 Oct 2026 09:32:54 -0700 Subject: [PATCH] fix(provider): recover parallel call arguments on done for all responses --- src/provider/codex-responses-sse.test.ts | 79 ++++++++++++++++++------ src/provider/codex-responses.ts | 11 ++-- src/provider/grok-responses.ts | 37 ++++++----- src/provider/inference-dependencies.ts | 7 ++- src/provider/openai-responses.ts | 7 ++- 5 files changed, 97 insertions(+), 44 deletions(-) diff --git a/src/provider/codex-responses-sse.test.ts b/src/provider/codex-responses-sse.test.ts index 1e13e9369..ecd17c3ba 100644 --- a/src/provider/codex-responses-sse.test.ts +++ b/src/provider/codex-responses-sse.test.ts @@ -7,6 +7,11 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { createCodexResponsesAdapter } from "./codex-responses.js"; +import { createOpenAIResponsesAdapter } from "./openai-responses.js"; +import { createGrokResponsesAdapter } from "./grok-responses.js"; +import { createInferenceDependencies } from "./inference-dependencies.js"; +import { META_PROVIDER } from "@corbits/meta-provider"; +import type { ProviderAdapter } from "@intx/inference"; import type { InferenceEvent, LastCycleSource } from "@intx/types/runtime"; import { defined } from "../../testkit/defined.js"; import { ProtocolMismatchError } from "@intx/inference"; @@ -29,8 +34,10 @@ function loadFixture(name: string): object[] { } /** Parse a full fixture stream with one adapter instance (shared indexer). */ -function parseFixture(name: string): InferenceEvent[] { - const adapter = createCodexResponsesAdapter(SOURCE); +function parseFixture( + name: string, + adapter: ProviderAdapter = createCodexResponsesAdapter(SOURCE), +): InferenceEvent[] { const out: InferenceEvent[] = []; for (const event of loadFixture(name)) { out.push(...adapter.parseResponse(JSON.stringify(event))); @@ -212,24 +219,58 @@ describe("codex-sse fixtures (golden parse)", () => { expect(String(error)).toMatch(/fixture stream error \(sanitized\)/); }); - test("parallel calls whose arguments arrive only on done still carry their arguments", () => { - const out = parseFixture("parallel-tools-arguments-on-done.json"); + test("parallel calls whose arguments arrive only on done still carry their arguments across all Responses adapters", async () => { + const deps = await createInferenceDependencies(); + const adapters: [string, ProviderAdapter][] = [ + ["codex-responses", createCodexResponsesAdapter(SOURCE)], + [ + "openai-responses", + createOpenAIResponsesAdapter({ + sourceId: "go/fixture", + provider: "openai-responses", + model: "gpt-fixture", + }), + ], + [ + "grok-responses", + createGrokResponsesAdapter({ + sourceId: "xai/fixture", + provider: "grok-responses", + model: "grok-fixture", + }), + ], + [ + "meta-responses", + deps.adapters.resolve({ + sourceId: "meta/fixture", + provider: META_PROVIDER, + model: "llama-fixture", + }), + ], + ]; - expect(eventTypes(out)).toEqual([ - "inference.tool_call.start", - "inference.tool_call.delta", - "inference.tool_call.start", - "inference.tool_call.delta", - "inference.usage", - ]); - expect(out[1]).toMatchObject({ - type: "inference.tool_call.delta", - data: { callId: "0", argumentFragment: '{"path":"a.txt"}', index: 0 }, - }); - expect(out[3]).toMatchObject({ - type: "inference.tool_call.delta", - data: { callId: "1", argumentFragment: '{"path":"b.txt"}', index: 1 }, - }); + for (const [label, adapter] of adapters) { + const out = parseFixture( + "parallel-tools-arguments-on-done.json", + adapter, + ); + + expect(eventTypes(out), label).toEqual([ + "inference.tool_call.start", + "inference.tool_call.delta", + "inference.tool_call.start", + "inference.tool_call.delta", + "inference.usage", + ]); + expect(out[1], label).toMatchObject({ + type: "inference.tool_call.delta", + data: { callId: "0", argumentFragment: '{"path":"a.txt"}', index: 0 }, + }); + expect(out[3], label).toMatchObject({ + type: "inference.tool_call.delta", + data: { callId: "1", argumentFragment: '{"path":"b.txt"}', index: 1 }, + }); + } }); test("lifecycle envelopes alone produce no InferenceEvents", () => { diff --git a/src/provider/codex-responses.ts b/src/provider/codex-responses.ts index fc7b7ed2a..b97cd1426 100644 --- a/src/provider/codex-responses.ts +++ b/src/provider/codex-responses.ts @@ -58,11 +58,12 @@ const FunctionCallItemDone = type({ item: { type: "'function_call'", call_id: "string", arguments: "string" }, }); -// With parallel calls on, some Codex backends send a call's arguments only on -// output_item.done and never as deltas. The packaged parser reads deltas -// alone, so without this replay the harness assembles those calls as `{}` -// (CL-10582). -function withArgumentsFromDone(adapter: ProviderAdapter): ProviderAdapter { +// With parallel calls on, some Responses backends send a call's arguments only +// on output_item.done and never as deltas. The packaged parser reads deltas +// alone, so without this replay the harness assembles those calls as `{}`. +export function withArgumentsFromDone( + adapter: ProviderAdapter, +): ProviderAdapter { let indexByCallId = new Map(); let streamedIndices = new Set(); return { diff --git a/src/provider/grok-responses.ts b/src/provider/grok-responses.ts index 590be89dd..2224034e0 100644 --- a/src/provider/grok-responses.ts +++ b/src/provider/grok-responses.ts @@ -1,6 +1,7 @@ import { responsesAdapterFactory } from "@corbits/openai-responses"; import { XAI_USER_ID_OPTION, xaiResponsesQuirks } from "@corbits/xai-provider"; import type { AdapterFactory } from "@intx/inference"; +import { withArgumentsFromDone } from "./codex-responses.js"; export const GROK_RESPONSES_PROVIDER = "grok-responses"; @@ -11,20 +12,22 @@ export const GROK_SESSION_ID_OPTION = "grokSessionId"; // grokUserId / grokSessionId / reasoning_effort; aliasing those in a // buildRequest wrapper would hide the mismatch instead of baking the host // names. -export const createGrokResponsesAdapter: AdapterFactory = - responsesAdapterFactory({ - ...xaiResponsesQuirks, - sessionIdOption: GROK_SESSION_ID_OPTION, - headers: { - ...xaiResponsesQuirks.headers, - fromOption: (xaiResponsesQuirks.headers?.fromOption ?? []).map((entry) => - entry.optionKey === XAI_USER_ID_OPTION - ? { ...entry, optionKey: GROK_USER_ID_OPTION } - : entry, - ), - }, - reasoning: { - ...xaiResponsesQuirks.reasoning, - effortOption: "reasoning_effort", - }, - }); +const baseFactory = responsesAdapterFactory({ + ...xaiResponsesQuirks, + sessionIdOption: GROK_SESSION_ID_OPTION, + headers: { + ...xaiResponsesQuirks.headers, + fromOption: (xaiResponsesQuirks.headers?.fromOption ?? []).map((entry) => + entry.optionKey === XAI_USER_ID_OPTION + ? { ...entry, optionKey: GROK_USER_ID_OPTION } + : entry, + ), + }, + reasoning: { + ...xaiResponsesQuirks.reasoning, + effortOption: "reasoning_effort", + }, +}); + +export const createGrokResponsesAdapter: AdapterFactory = (source) => + withArgumentsFromDone(baseFactory(source)); diff --git a/src/provider/inference-dependencies.ts b/src/provider/inference-dependencies.ts index f4fd34b55..cf09a62fa 100644 --- a/src/provider/inference-dependencies.ts +++ b/src/provider/inference-dependencies.ts @@ -13,6 +13,7 @@ import * as openaiResponses from "./openai-responses.js"; import * as anthropicSession from "./anthropic-session-adapter.js"; import { CODEX_RESPONSES_PROVIDER, + withArgumentsFromDone, withCodexContentTypeRepair, } from "./codex-responses.js"; import { GROK_RESPONSES_PROVIDER } from "./grok-responses.js"; @@ -95,7 +96,11 @@ const localModules: Record = { "opencode-go-adapter": opencodeGo, "codex-responses": codexResponses, "grok-responses": grokResponses, - "meta-responses": { createMetaResponsesAdapter }, + "meta-responses": { + createMetaResponsesAdapter: ( + ...args: Parameters + ) => withArgumentsFromDone(createMetaResponsesAdapter(...args)), + }, "bifrost-adapter": bifrostAdapter, "deepseek-v4-adapter": deepseekV4, "openai-responses": openaiResponses, diff --git a/src/provider/openai-responses.ts b/src/provider/openai-responses.ts index 23452c2c2..c40374c08 100644 --- a/src/provider/openai-responses.ts +++ b/src/provider/openai-responses.ts @@ -3,6 +3,7 @@ import { type ResponsesQuirks, } from "@corbits/openai-responses"; import type { AdapterFactory } from "@intx/inference"; +import { withArgumentsFromDone } from "./codex-responses.js"; import { OPENCODE_SESSION_ID_OPTION } from "./opencode-session.js"; export const OPENAI_RESPONSES_PROVIDER = "openai-responses"; @@ -24,5 +25,7 @@ export const hostQuirks: ResponsesQuirks = { reasoning: { summary: "auto" }, }; -export const createOpenAIResponsesAdapter: AdapterFactory = - responsesAdapterFactory(hostQuirks); +const baseFactory = responsesAdapterFactory(hostQuirks); + +export const createOpenAIResponsesAdapter: AdapterFactory = (source) => + withArgumentsFromDone(baseFactory(source));