mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 20:32:25 +03:00
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:
1
changelog.d/fixes/12909-tool-call-history-preserve.md
Normal file
1
changelog.d/fixes/12909-tool-call-history-preserve.md
Normal 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))
|
||||
@@ -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;
|
||||
|
||||
68
tests/unit/translator-openai-responses-tool-calls.test.ts
Normal file
68
tests/unit/translator-openai-responses-tool-calls.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user