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
97 changes: 93 additions & 4 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,61 @@ function toolOutputText(output: unknown): string {
}).filter(Boolean).join("\n");
}

/** True when an output can be losslessly represented as user-message content. */
function isRepairableToolOutput(output: unknown): output is string | Record<string, unknown>[] {
if (typeof output === "string") return true;
if (!Array.isArray(output)) return false;
return output.every(part => {
if (!isPlainObject(part)) return false;
if (typeof part.type !== "string") return false;
if (["output_text", "text", "input_text"].includes(part.type)) {
return typeof part.text === "string";
}
if (part.type === "refusal") return typeof part.refusal === "string";
if (part.type === "encrypted_content") return typeof part.encrypted_content === "string";
if (part.type !== "input_image") return false;
const imageUrl = part.image_url;
const fileId = part.file_id;
const imageUrlIsString = typeof imageUrl === "string";
const fileIdIsString = typeof fileId === "string";
const hasUsableSource = (imageUrlIsString && imageUrl.length > 0)
|| (fileIdIsString && fileId.length > 0);
const validSource = hasUsableSource
&& (part.image_url === undefined || imageUrlIsString)
&& (part.file_id === undefined || fileIdIsString);
const validDetail = part.detail === undefined
|| (typeof part.detail === "string"
&& ["auto", "low", "high", "original"].includes(part.detail));
return validSource && validDetail;
});
}

/** Convert orphaned tool output to user-message content without discarding valid images. */
function orphanedToolOutputContent(output: unknown, callId = ""): Record<string, unknown>[] {
const marker = `[tool output for ${callId || "unknown call"}]`;
if (typeof output !== "string" && !Array.isArray(output)) {
return [{ type: "input_text", text: marker }];
}
if (!Array.isArray(output)) {
return [{ type: "input_text", text: `${marker}\n${toolOutputText(output)}` }];
}

const content: Record<string, unknown>[] = [{ type: "input_text", text: marker }];
for (const part of output) {
if (!isPlainObject(part)) continue;
if (part.type === "input_image") {
content.push(part);
} else if (part.type === "encrypted_content" && typeof part.encrypted_content === "string") {
content.push({ type: "input_text", text: "[encrypted content omitted]" });
} else if (typeof part.text === "string") {
content.push({ type: "input_text", text: part.text });
Comment thread
ildunari marked this conversation as resolved.
} else if (part.type === "refusal" && typeof part.refusal === "string") {
content.push({ type: "input_text", text: `[refusal] ${part.refusal}` });
}
}
return content;
}

/** True when a Responses tool output item is present but carries no usable content. */
function isToolOutputEmpty(output: unknown): boolean {
if (typeof output === "string") return output.trim() === "";
Expand Down Expand Up @@ -940,6 +995,32 @@ function annotateEmptyResponsesToolOutputs(body: unknown, enabled: boolean): unk
return changed ? { ...body, input } : body;
}

/**
* Preserve the text of structurally invalid tool-output items before they reach a strict
* Responses parser. Stateful destinations may legitimately receive an output whose matching
* call lives behind `previous_response_id`, so ordinary orphan repair cannot run universally.
* A missing or empty `call_id`, however, cannot identify stored state on any destination.
*/
function repairUnidentifiedToolOutputItems(body: unknown): unknown {
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
let changed = false;
const input = body.input.map(item => {
if (!isPlainObject(item)
|| (item.type !== "function_call_output" && item.type !== "custom_tool_call_output")
|| (typeof item.call_id === "string" && item.call_id.length > 0)) {
return item;
}
if (!isRepairableToolOutput(item.output)) return item;
changed = true;
return {
type: "message",
role: "user",
content: orphanedToolOutputContent(item.output),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
});
return changed ? { ...body, input } : body;
}

/**
* Repair a forward-mode input array whose continuation context was lost. When the replay
* expansion misses (proxy restart, unrecorded prior turn), previous_response_id is stripped
Expand Down Expand Up @@ -1060,12 +1141,17 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthes
flushPendingSyntheticOutputs();
const callId = typeof item.call_id === "string" ? item.call_id : "";
const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
if (!paired) {
const usableOutput = isRepairableToolOutput(item.output);
// A known orphan call is still useful as a labeled user message even when its output is
// incomplete. With no call id and no output, preserve the invalid item so validation fails
// closed rather than pretending any tool result exists.
const knownNullOutput = callId.length > 0 && item.output == null;
if (!paired && (knownNullOutput || usableOutput)) {
changed = true;
repaired.push({
type: "message",
role: "user",
content: [{ type: "input_text", text: `[tool output for ${callId || "unknown call"}]\n${toolOutputText(item.output)}` }],
content: orphanedToolOutputContent(item.output, callId),
});
continue;
}
Expand Down Expand Up @@ -2272,11 +2358,14 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
// Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
// let a noncanonical custom forward provider skip this rewrite while the server still routes
// it as a summarizer turn (#422). The compaction body build removes the tool surface and must
// therefore be the last routed transform: anything before it may depend on the declarations;
// anything after it cannot.
// therefore be the last routed transform that may depend on those declarations. Structural
// sanitizers below can still run after it.
if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
outBody = buildRoutedCompactionBody(outBody);
}
// Run after routed compaction so nested input_image parts are replaced before a malformed
// tool output is flattened to text and can no longer be inspected structurally.
outBody = repairUnidentifiedToolOutputItems(outBody);
Comment thread
ildunari marked this conversation as resolved.
const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
const sanitizedBody = normalizeToolSchemas(
stripSparkCompatibility(
Expand Down
141 changes: 139 additions & 2 deletions tests/openai-responses-passthrough.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2245,8 +2245,137 @@ describe("OpenAI Responses passthrough sanitization", () => {
expect(rawDeltaBody.input).toHaveLength(1);
});

test("api-key mode preserves delegated tool output text when call_id is missing", () => {
const adapter = createResponsesPassthroughAdapter({
adapter: "openai-responses",
baseUrl: "https://api.x.ai/v1",
authMode: "key" as const,
apiKey: "xai-test",
});

const body = JSON.parse(adapter.buildRequest({
...parsedBase,
previousResponseId: undefined,
_rawBody: {
model: "grok-4.6",
input: [
{
type: "function_call_output",
id: "fco_delegation",
output: "<codex_delegation>Inspect the adapter.</codex_delegation>",
},
],
},
}, meta).body) as { input: Record<string, unknown>[] };

expect(body.input).toEqual([{
type: "message",
role: "user",
content: [{
type: "input_text",
text: "[tool output for unknown call]\n<codex_delegation>Inspect the adapter.</codex_delegation>",
}],
}]);
});

test("api-key mode keeps stateful tool outputs with call_id intact", () => {
const adapter = createResponsesPassthroughAdapter({
adapter: "openai-responses",
baseUrl: "https://api.x.ai/v1",
authMode: "key" as const,
apiKey: "xai-test",
});
const statefulOutput = {
type: "function_call_output",
call_id: "call_from_previous_response",
output: "tool result",
};

const body = JSON.parse(adapter.buildRequest({
...parsedBase,
_rawBody: {
model: "grok-4.6",
previous_response_id: "resp_stateful",
input: [statefulOutput],
},
}, meta).body) as { previous_response_id?: string; input: Record<string, unknown>[] };

expect(body.previous_response_id).toBe("resp_stateful");
expect(body.input).toEqual([statefulOutput]);
});

test("api-key mode preserves images when repairing output without call_id", () => {
const adapter = createResponsesPassthroughAdapter({
adapter: "openai-responses",
baseUrl: "https://api.x.ai/v1",
authMode: "key" as const,
apiKey: "xai-test",
});
const image = {
type: "input_image",
image_url: "data:image/png;base64,AAAA",
detail: "high",
};

const body = JSON.parse(adapter.buildRequest({
...parsedBase,
_rawBody: {
model: "grok-4.6",
input: [{
type: "function_call_output",
output: [
{ type: "input_text", text: "screenshot" },
image,
{ type: "encrypted_content", encrypted_content: "opaque-tool-state" },
],
}],
},
}, meta).body) as { input: Array<{ content: Record<string, unknown>[] }> };

expect(body.input[0]?.content).toEqual([
{ type: "input_text", text: "[tool output for unknown call]" },
{ type: "input_text", text: "screenshot" },
image,
{ type: "input_text", text: "[encrypted content omitted]" },
]);
});

test("api-key mode leaves invalid output without call_id fail-closed", () => {
const adapter = createResponsesPassthroughAdapter({
adapter: "openai-responses",
baseUrl: "https://api.x.ai/v1",
authMode: "key" as const,
apiKey: "xai-test",
});
const invalidOutputs = [
{ type: "custom_tool_call_output" },
{ type: "function_call_output", output: [{ type: "bogus", value: "not a tool-output part" }] },
{
type: "function_call_output",
output: [{ type: "input_image", image_url: "data:image/png;base64,AAAA", detail: ["high"] }],
},
{
type: "function_call_output",
output: [{ type: "input_image", image_url: 42, file_id: "file_1" }],
},
{ type: "function_call_output", output: [{ type: "input_image", image_url: "" }] },
{
type: "function_call_output",
output: [{ type: { toString: null, valueOf: null }, text: "must not coerce" }],
},
];

const body = JSON.parse(adapter.buildRequest({
...parsedBase,
_rawBody: { model: "grok-4.6", input: invalidOutputs },
}, meta).body) as { input: Record<string, unknown>[] };

expect(body.input).toEqual(invalidOutputs);
});

test("forward unexpanded miss converts orphan tool outputs and drops reasoning", () => {
const adapter = createResponsesPassthroughAdapter(provider);
const image = { type: "input_image", image_url: "data:image/png;base64,AAAA" };
const body = JSON.parse(adapter.buildRequest({
...parsedBase,
_rawBody: {
Expand All @@ -2255,7 +2384,11 @@ describe("OpenAI Responses passthrough sanitization", () => {
input: [
{ type: "reasoning", id: "rs_1", summary: [] },
{ type: "function_call_output", call_id: "call_orphan", output: "tool said hi" },
{ type: "custom_tool_call_output", call_id: "call_custom", output: [{ type: "output_text", text: "custom out" }] },
{
type: "custom_tool_call_output",
call_id: "call_custom",
output: [{ type: "output_text", text: "custom out" }, image],
},
{ role: "user", content: "next question" },
],
},
Expand All @@ -2272,7 +2405,11 @@ describe("OpenAI Responses passthrough sanitization", () => {
expect(body.input[1]).toMatchObject({
type: "message",
role: "user",
content: [{ type: "input_text", text: "[tool output for call_custom]\ncustom out" }],
content: [
{ type: "input_text", text: "[tool output for call_custom]" },
{ type: "input_text", text: "custom out" },
image,
],
});
expect(body.input[2]).toMatchObject({ role: "user", content: "next question" });
});
Expand Down
Loading