Skip to content
Merged
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
79 changes: 60 additions & 19 deletions src/provider/codex-responses-sse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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)));
Expand Down Expand Up @@ -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", () => {
Expand Down
11 changes: 6 additions & 5 deletions src/provider/codex-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>();
let streamedIndices = new Set<number>();
return {
Expand Down
37 changes: 20 additions & 17 deletions src/provider/grok-responses.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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));
7 changes: 6 additions & 1 deletion src/provider/inference-dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -95,7 +96,11 @@ const localModules: Record<string, unknown> = {
"opencode-go-adapter": opencodeGo,
"codex-responses": codexResponses,
"grok-responses": grokResponses,
"meta-responses": { createMetaResponsesAdapter },
"meta-responses": {
createMetaResponsesAdapter: (
...args: Parameters<typeof createMetaResponsesAdapter>
) => withArgumentsFromDone(createMetaResponsesAdapter(...args)),
},
"bifrost-adapter": bifrostAdapter,
"deepseek-v4-adapter": deepseekV4,
"openai-responses": openaiResponses,
Expand Down
7 changes: 5 additions & 2 deletions src/provider/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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));
Loading