|
5 | 5 | CODEX_SESSION_ID_OPTION, |
6 | 6 | withCodexContentTypeRepair, |
7 | 7 | } from "@corbits/codex-provider"; |
| 8 | +import { type } from "arktype"; |
8 | 9 | import type { AdapterFactory, ProviderAdapter } from "@intx/inference"; |
9 | 10 | import type { InferenceOptions } from "@intx/types/runtime"; |
10 | 11 | import { ENVIRONMENT_TAG_NAME, PRODUCT_NAME } from "../branding.js"; |
@@ -73,11 +74,70 @@ export function withParallelToolCalls( |
73 | 74 | }; |
74 | 75 | } |
75 | 76 |
|
| 77 | +const FunctionCallItemDone = type({ |
| 78 | + type: "'response.output_item.done'", |
| 79 | + item: { type: "'function_call'", call_id: "string", arguments: "string" }, |
| 80 | +}); |
| 81 | + |
| 82 | +// With parallel calls on, some Codex backends send a call's arguments only on |
| 83 | +// output_item.done and never as deltas. The packaged parser reads deltas |
| 84 | +// alone, so without this replay the harness assembles those calls as `{}` |
| 85 | +// (CL-10582). |
| 86 | +function withArgumentsFromDone(adapter: ProviderAdapter): ProviderAdapter { |
| 87 | + let indexByCallId = new Map<string, number>(); |
| 88 | + let streamedIndices = new Set<number>(); |
| 89 | + return { |
| 90 | + ...adapter, |
| 91 | + buildRequest: (messages, model, options) => { |
| 92 | + indexByCallId = new Map(); |
| 93 | + streamedIndices = new Set(); |
| 94 | + return adapter.buildRequest(messages, model, options); |
| 95 | + }, |
| 96 | + parseResponse: (sseData) => { |
| 97 | + const events = adapter.parseResponse(sseData); |
| 98 | + for (const event of events) { |
| 99 | + if (event.type === "inference.tool_call.start") { |
| 100 | + if (event.data.index !== undefined) { |
| 101 | + indexByCallId.set(event.data.callId, event.data.index); |
| 102 | + } |
| 103 | + } else if (event.type === "inference.tool_call.delta") { |
| 104 | + if (event.data.index !== undefined) { |
| 105 | + streamedIndices.add(event.data.index); |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + if (!sseData.includes('"response.output_item.done"')) return events; |
| 110 | + const done = FunctionCallItemDone(JSON.parse(sseData)); |
| 111 | + if (done instanceof type.errors || done.item.arguments.length === 0) { |
| 112 | + return events; |
| 113 | + } |
| 114 | + const index = indexByCallId.get(done.item.call_id); |
| 115 | + if (index === undefined || streamedIndices.has(index)) return events; |
| 116 | + streamedIndices.add(index); |
| 117 | + return [ |
| 118 | + ...events, |
| 119 | + { |
| 120 | + type: "inference.tool_call.delta", |
| 121 | + seq: 0, |
| 122 | + data: { |
| 123 | + callId: String(index), |
| 124 | + argumentFragment: done.item.arguments, |
| 125 | + partial: { text: "" }, |
| 126 | + index, |
| 127 | + }, |
| 128 | + }, |
| 129 | + ]; |
| 130 | + }, |
| 131 | + }; |
| 132 | +} |
| 133 | + |
76 | 134 | // CodexQuirks are required by the package factory; Responses option keys are |
77 | 135 | // not part of that bag, so host reasoning_effort is aliased at buildRequest. |
78 | 136 | export const createCodexResponsesAdapter: AdapterFactory = (source) => |
79 | 137 | withHostReasoningEffort( |
80 | 138 | withParallelToolCalls( |
81 | | - createPackageCodexResponsesAdapter(source, CODEX_QUIRKS), |
| 139 | + withArgumentsFromDone( |
| 140 | + createPackageCodexResponsesAdapter(source, CODEX_QUIRKS), |
| 141 | + ), |
82 | 142 | ), |
83 | 143 | ); |
0 commit comments