fix(compat): preserve GPT and Claude Code tool-call history (#12909)

* fix(compat): preserve GPT and Claude Code tool-call history

* docs(changelog): add fragment for tool-call history preservation fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
initguru
2026-09-17 14:29:39 +09:00
committed by GitHub
parent 051576fd3d
commit 9febe4414d
3 changed files with 112 additions and 1 deletions

View File

@@ -0,0 +1 @@
- **fix(compat):** preserve GPT and Claude Code tool-call history when translating OpenAI Responses API requests to Chat Completions — `role:"tool"` items and role-based assistant `tool_calls` are no longer dropped, fixing incomplete multi-turn history ([#12909](https://github.com/diegosouzapw/OmniRoute/pull/12909))

View File

@@ -79,6 +79,29 @@ function appendReasoningContent(current: unknown, next: string): string {
return existing ? `${existing}\n\n${next}` : next;
}
function normalizeRoleBasedToolCalls(toolCalls: unknown): JsonRecord[] {
if (!Array.isArray(toolCalls)) return [];
return toolCalls
.map((toolCallValue) => {
const toolCall = toRecord(toolCallValue);
const fn = toRecord(toolCall.function);
const name = toString(fn.name).trim();
const id = toString(toolCall.id).trim();
if (!name || !id) return null;
return {
id,
type: "function",
function: {
name,
arguments:
typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments ?? {}),
},
};
})
.filter((toolCall): toolCall is JsonRecord => toolCall !== null);
}
/**
* Convert OpenAI Responses API request to OpenAI Chat Completions format
*/
@@ -252,6 +275,15 @@ export function openaiResponsesToOpenAIRequest(
pendingToolResults = [];
}
if (toString(item.role) === "tool") {
messages.push({
role: "tool",
tool_call_id: toString(item.tool_call_id),
content: toolOutputContentToString(item.content),
});
continue;
}
// Convert content: input_text -> text, output_text -> text
const content = Array.isArray(item.content)
? item.content.map((contentValue) => {
@@ -288,7 +320,17 @@ export function openaiResponsesToOpenAIRequest(
: item.content;
if (role === "assistant") {
if (!currentAssistantMsg) {
const roleBasedToolCalls = normalizeRoleBasedToolCalls(item.tool_calls);
if (roleBasedToolCalls.length > 0) {
if (currentAssistantMsg) {
messages.push(currentAssistantMsg);
}
currentAssistantMsg = {
role,
content,
tool_calls: roleBasedToolCalls,
};
} else if (!currentAssistantMsg) {
currentAssistantMsg = { role, content };
} else if (currentAssistantMsg.content == null && content != null) {
currentAssistantMsg.content = content;

View File

@@ -0,0 +1,68 @@
import test from "node:test";
import assert from "node:assert/strict";
const { openaiResponsesToOpenAIRequest } =
await import("../../open-sse/translator/request/openai-responses.ts");
test("Responses -> Chat preserves role-based assistant tool_calls and tool results", () => {
const result = openaiResponsesToOpenAIRequest(
"deepseek-v4-flash",
{
model: "deepseek-v4-flash",
input: [
{ role: "user", content: "Run pwd" },
{
role: "assistant",
content: null,
tool_calls: [
{
id: "call_1",
type: "function",
function: { name: "exec_command", arguments: '{"cmd":"pwd"}' },
},
],
},
{ role: "tool", tool_call_id: "call_1", content: "/tmp" },
],
},
false,
{ provider: "deepseek" }
) as { messages: Array<Record<string, unknown>> };
assert.deepEqual(result.messages[1].tool_calls, [
{
id: "call_1",
type: "function",
function: { name: "exec_command", arguments: '{"cmd":"pwd"}' },
},
]);
assert.deepEqual(result.messages[2], {
role: "tool",
tool_call_id: "call_1",
content: "/tmp",
});
});
test("Responses -> Chat drops role-based tool_calls with empty name or id", () => {
const result = openaiResponsesToOpenAIRequest(
"deepseek-v4-flash",
{
model: "deepseek-v4-flash",
input: [
{ role: "user", content: "Run" },
{
role: "assistant",
content: null,
tool_calls: [
{ id: "call_nameless", type: "function", function: { name: "", arguments: "{}" } },
{ id: "", type: "function", function: { name: "exec_command", arguments: "{}" } },
],
},
],
},
false,
{ provider: "deepseek" }
) as { messages: Array<Record<string, unknown>> };
assert.equal(result.messages[1].tool_calls, undefined);
});