Skip to content

Commit b483001

Browse files
Merge pull request #1556 from corbitsdev/cl-11324-recover-parallel-tool-call-arguments-sent-only-on
fix(provider): recover parallel call arguments on done for all responses
2 parents afa244c + 27a0c54 commit b483001

5 files changed

Lines changed: 97 additions & 44 deletions

File tree

‎src/provider/codex-responses-sse.test.ts‎

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import { describe, expect, test } from "bun:test";
77
import { readFileSync } from "node:fs";
88
import { join } from "node:path";
99
import { createCodexResponsesAdapter } from "./codex-responses.js";
10+
import { createOpenAIResponsesAdapter } from "./openai-responses.js";
11+
import { createGrokResponsesAdapter } from "./grok-responses.js";
12+
import { createInferenceDependencies } from "./inference-dependencies.js";
13+
import { META_PROVIDER } from "@corbits/meta-provider";
14+
import type { ProviderAdapter } from "@intx/inference";
1015
import type { InferenceEvent, LastCycleSource } from "@intx/types/runtime";
1116
import { defined } from "../../testkit/defined.js";
1217
import { ProtocolMismatchError } from "@intx/inference";
@@ -29,8 +34,10 @@ function loadFixture(name: string): object[] {
2934
}
3035

3136
/** Parse a full fixture stream with one adapter instance (shared indexer). */
32-
function parseFixture(name: string): InferenceEvent[] {
33-
const adapter = createCodexResponsesAdapter(SOURCE);
37+
function parseFixture(
38+
name: string,
39+
adapter: ProviderAdapter = createCodexResponsesAdapter(SOURCE),
40+
): InferenceEvent[] {
3441
const out: InferenceEvent[] = [];
3542
for (const event of loadFixture(name)) {
3643
out.push(...adapter.parseResponse(JSON.stringify(event)));
@@ -212,24 +219,58 @@ describe("codex-sse fixtures (golden parse)", () => {
212219
expect(String(error)).toMatch(/fixture stream error \(sanitized\)/);
213220
});
214221

215-
test("parallel calls whose arguments arrive only on done still carry their arguments", () => {
216-
const out = parseFixture("parallel-tools-arguments-on-done.json");
222+
test("parallel calls whose arguments arrive only on done still carry their arguments across all Responses adapters", async () => {
223+
const deps = await createInferenceDependencies();
224+
const adapters: [string, ProviderAdapter][] = [
225+
["codex-responses", createCodexResponsesAdapter(SOURCE)],
226+
[
227+
"openai-responses",
228+
createOpenAIResponsesAdapter({
229+
sourceId: "go/fixture",
230+
provider: "openai-responses",
231+
model: "gpt-fixture",
232+
}),
233+
],
234+
[
235+
"grok-responses",
236+
createGrokResponsesAdapter({
237+
sourceId: "xai/fixture",
238+
provider: "grok-responses",
239+
model: "grok-fixture",
240+
}),
241+
],
242+
[
243+
"meta-responses",
244+
deps.adapters.resolve({
245+
sourceId: "meta/fixture",
246+
provider: META_PROVIDER,
247+
model: "llama-fixture",
248+
}),
249+
],
250+
];
217251

218-
expect(eventTypes(out)).toEqual([
219-
"inference.tool_call.start",
220-
"inference.tool_call.delta",
221-
"inference.tool_call.start",
222-
"inference.tool_call.delta",
223-
"inference.usage",
224-
]);
225-
expect(out[1]).toMatchObject({
226-
type: "inference.tool_call.delta",
227-
data: { callId: "0", argumentFragment: '{"path":"a.txt"}', index: 0 },
228-
});
229-
expect(out[3]).toMatchObject({
230-
type: "inference.tool_call.delta",
231-
data: { callId: "1", argumentFragment: '{"path":"b.txt"}', index: 1 },
232-
});
252+
for (const [label, adapter] of adapters) {
253+
const out = parseFixture(
254+
"parallel-tools-arguments-on-done.json",
255+
adapter,
256+
);
257+
258+
expect(eventTypes(out), label).toEqual([
259+
"inference.tool_call.start",
260+
"inference.tool_call.delta",
261+
"inference.tool_call.start",
262+
"inference.tool_call.delta",
263+
"inference.usage",
264+
]);
265+
expect(out[1], label).toMatchObject({
266+
type: "inference.tool_call.delta",
267+
data: { callId: "0", argumentFragment: '{"path":"a.txt"}', index: 0 },
268+
});
269+
expect(out[3], label).toMatchObject({
270+
type: "inference.tool_call.delta",
271+
data: { callId: "1", argumentFragment: '{"path":"b.txt"}', index: 1 },
272+
});
273+
}
233274
});
234275

235276
test("lifecycle envelopes alone produce no InferenceEvents", () => {

‎src/provider/codex-responses.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,12 @@ const FunctionCallItemDone = type({
5858
item: { type: "'function_call'", call_id: "string", arguments: "string" },
5959
});
6060

61-
// With parallel calls on, some Codex backends send a call's arguments only on
62-
// output_item.done and never as deltas. The packaged parser reads deltas
63-
// alone, so without this replay the harness assembles those calls as `{}`
64-
// (CL-10582).
65-
function withArgumentsFromDone(adapter: ProviderAdapter): ProviderAdapter {
61+
// With parallel calls on, some Responses backends send a call's arguments only
62+
// on output_item.done and never as deltas. The packaged parser reads deltas
63+
// alone, so without this replay the harness assembles those calls as `{}`.
64+
export function withArgumentsFromDone(
65+
adapter: ProviderAdapter,
66+
): ProviderAdapter {
6667
let indexByCallId = new Map<string, number>();
6768
let streamedIndices = new Set<number>();
6869
return {

‎src/provider/grok-responses.ts‎

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { responsesAdapterFactory } from "@corbits/openai-responses";
22
import { XAI_USER_ID_OPTION, xaiResponsesQuirks } from "@corbits/xai-provider";
33
import type { AdapterFactory } from "@intx/inference";
4+
import { withArgumentsFromDone } from "./codex-responses.js";
45

56
export const GROK_RESPONSES_PROVIDER = "grok-responses";
67

@@ -11,20 +12,22 @@ export const GROK_SESSION_ID_OPTION = "grokSessionId";
1112
// grokUserId / grokSessionId / reasoning_effort; aliasing those in a
1213
// buildRequest wrapper would hide the mismatch instead of baking the host
1314
// names.
14-
export const createGrokResponsesAdapter: AdapterFactory =
15-
responsesAdapterFactory({
16-
...xaiResponsesQuirks,
17-
sessionIdOption: GROK_SESSION_ID_OPTION,
18-
headers: {
19-
...xaiResponsesQuirks.headers,
20-
fromOption: (xaiResponsesQuirks.headers?.fromOption ?? []).map((entry) =>
21-
entry.optionKey === XAI_USER_ID_OPTION
22-
? { ...entry, optionKey: GROK_USER_ID_OPTION }
23-
: entry,
24-
),
25-
},
26-
reasoning: {
27-
...xaiResponsesQuirks.reasoning,
28-
effortOption: "reasoning_effort",
29-
},
30-
});
15+
const baseFactory = responsesAdapterFactory({
16+
...xaiResponsesQuirks,
17+
sessionIdOption: GROK_SESSION_ID_OPTION,
18+
headers: {
19+
...xaiResponsesQuirks.headers,
20+
fromOption: (xaiResponsesQuirks.headers?.fromOption ?? []).map((entry) =>
21+
entry.optionKey === XAI_USER_ID_OPTION
22+
? { ...entry, optionKey: GROK_USER_ID_OPTION }
23+
: entry,
24+
),
25+
},
26+
reasoning: {
27+
...xaiResponsesQuirks.reasoning,
28+
effortOption: "reasoning_effort",
29+
},
30+
});
31+
32+
export const createGrokResponsesAdapter: AdapterFactory = (source) =>
33+
withArgumentsFromDone(baseFactory(source));

‎src/provider/inference-dependencies.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as openaiResponses from "./openai-responses.js";
1313
import * as anthropicSession from "./anthropic-session-adapter.js";
1414
import {
1515
CODEX_RESPONSES_PROVIDER,
16+
withArgumentsFromDone,
1617
withCodexContentTypeRepair,
1718
} from "./codex-responses.js";
1819
import { GROK_RESPONSES_PROVIDER } from "./grok-responses.js";
@@ -95,7 +96,11 @@ const localModules: Record<string, unknown> = {
9596
"opencode-go-adapter": opencodeGo,
9697
"codex-responses": codexResponses,
9798
"grok-responses": grokResponses,
98-
"meta-responses": { createMetaResponsesAdapter },
99+
"meta-responses": {
100+
createMetaResponsesAdapter: (
101+
...args: Parameters<typeof createMetaResponsesAdapter>
102+
) => withArgumentsFromDone(createMetaResponsesAdapter(...args)),
103+
},
99104
"bifrost-adapter": bifrostAdapter,
100105
"deepseek-v4-adapter": deepseekV4,
101106
"openai-responses": openaiResponses,

‎src/provider/openai-responses.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type ResponsesQuirks,
44
} from "@corbits/openai-responses";
55
import type { AdapterFactory } from "@intx/inference";
6+
import { withArgumentsFromDone } from "./codex-responses.js";
67
import { OPENCODE_SESSION_ID_OPTION } from "./opencode-session.js";
78

89
export const OPENAI_RESPONSES_PROVIDER = "openai-responses";
@@ -24,5 +25,7 @@ export const hostQuirks: ResponsesQuirks = {
2425
reasoning: { summary: "auto" },
2526
};
2627

27-
export const createOpenAIResponsesAdapter: AdapterFactory =
28-
responsesAdapterFactory(hostQuirks);
28+
const baseFactory = responsesAdapterFactory(hostQuirks);
29+
30+
export const createOpenAIResponsesAdapter: AdapterFactory = (source) =>
31+
withArgumentsFromDone(baseFactory(source));

0 commit comments

Comments
 (0)