Skip to content

Commit b2cb096

Browse files
committed
fix(provider): recover parallel call arguments on done for all responses
1 parent afa244c commit b2cb096

5 files changed

Lines changed: 105 additions & 44 deletions

File tree

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

Lines changed: 64 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,15 +34,21 @@ 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 parseFixtureWithAdapter(
38+
adapter: ProviderAdapter,
39+
name: string,
40+
): InferenceEvent[] {
3441
const out: InferenceEvent[] = [];
3542
for (const event of loadFixture(name)) {
3643
out.push(...adapter.parseResponse(JSON.stringify(event)));
3744
}
3845
return out;
3946
}
4047

48+
function parseFixture(name: string): InferenceEvent[] {
49+
return parseFixtureWithAdapter(createCodexResponsesAdapter(SOURCE), name);
50+
}
51+
4152
/** Parse until the first throw; return events emitted before the failure. */
4253
function parseUntilError(name: string): {
4354
events: InferenceEvent[];
@@ -212,24 +223,58 @@ describe("codex-sse fixtures (golden parse)", () => {
212223
expect(String(error)).toMatch(/fixture stream error \(sanitized\)/);
213224
});
214225

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

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-
});
256+
for (const [label, adapter] of adapters) {
257+
const out = parseFixtureWithAdapter(
258+
adapter,
259+
"parallel-tools-arguments-on-done.json",
260+
);
261+
262+
expect(eventTypes(out), label).toEqual([
263+
"inference.tool_call.start",
264+
"inference.tool_call.delta",
265+
"inference.tool_call.start",
266+
"inference.tool_call.delta",
267+
"inference.usage",
268+
]);
269+
expect(out[1], label).toMatchObject({
270+
type: "inference.tool_call.delta",
271+
data: { callId: "0", argumentFragment: '{"path":"a.txt"}', index: 0 },
272+
});
273+
expect(out[3], label).toMatchObject({
274+
type: "inference.tool_call.delta",
275+
data: { callId: "1", argumentFragment: '{"path":"b.txt"}', index: 1 },
276+
});
277+
}
233278
});
234279

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

‎src/provider/codex-responses.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,13 @@ 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
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
6363
// alone, so without this replay the harness assembles those calls as `{}`
64-
// (CL-10582).
65-
function withArgumentsFromDone(adapter: ProviderAdapter): ProviderAdapter {
64+
// (CL-10582, CL-11324).
65+
export function withArgumentsFromDone(
66+
adapter: ProviderAdapter,
67+
): ProviderAdapter {
6668
let indexByCallId = new Map<string, number>();
6769
let streamedIndices = new Set<number>();
6870
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 createPackageGrokResponsesAdapter = 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(createPackageGrokResponsesAdapter(source));

‎src/provider/inference-dependencies.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { createDependencies, type Dependencies } from "@intx/inference";
1+
import {
2+
createDependencies,
3+
type AdapterFactory,
4+
type Dependencies,
5+
} from "@intx/inference";
26
import { loadAdapterRegistry } from "@intx/inference/providers";
37
// AdapterManifest moved from the @intx/inference barrel to @intx/types at
48
// upstream 4cc6972 (the inference barrel no longer re-exports it).
@@ -13,12 +17,13 @@ import * as openaiResponses from "./openai-responses.js";
1317
import * as anthropicSession from "./anthropic-session-adapter.js";
1418
import {
1519
CODEX_RESPONSES_PROVIDER,
20+
withArgumentsFromDone,
1621
withCodexContentTypeRepair,
1722
} from "./codex-responses.js";
1823
import { GROK_RESPONSES_PROVIDER } from "./grok-responses.js";
1924
import {
2025
META_PROVIDER,
21-
createMetaResponsesAdapter,
26+
createMetaResponsesAdapter as createPackageMetaResponsesAdapter,
2227
} from "@corbits/meta-provider";
2328
import { withAnthropicCacheBreakpoint } from "./anthropic-cache-breakpoint.js";
2429
import { withReplaySanitizer } from "./replay-sanitizer.js";
@@ -90,6 +95,9 @@ const manifest: AdapterManifest = [
9095
},
9196
];
9297

98+
const createMetaResponsesAdapter: AdapterFactory = (source) =>
99+
withArgumentsFromDone(createPackageMetaResponsesAdapter(source));
100+
93101
const localModules: Record<string, unknown> = {
94102
"openai-compatible-adapter": openaiCompatible,
95103
"opencode-go-adapter": opencodeGo,

‎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 createPackageOpenAIResponsesAdapter = responsesAdapterFactory(hostQuirks);
29+
30+
export const createOpenAIResponsesAdapter: AdapterFactory = (source) =>
31+
withArgumentsFromDone(createPackageOpenAIResponsesAdapter(source));

0 commit comments

Comments
 (0)