From 01e7fca71670f0cdb3947abc80f35a6bc07a5bb7 Mon Sep 17 00:00:00 2001 From: pick-cat Date: Tue, 4 Aug 2026 14:03:44 +0800 Subject: [PATCH] fix(ai): keep OpenAI tool call id truncation UTF-16 safe (#110471) --- .../src/openai-completions-messages.test.ts | 48 +++++++++++++++++++ .../ai/src/openai-completions-messages.ts | 3 +- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/openai-completions-messages.test.ts b/packages/ai/src/openai-completions-messages.test.ts index 42e552d7c7ce..6f538448e1eb 100644 --- a/packages/ai/src/openai-completions-messages.test.ts +++ b/packages/ai/src/openai-completions-messages.test.ts @@ -49,4 +49,52 @@ describe("convertMessages assistant text replay", () => { const replayed = converted.find((message) => message.role === "assistant"); expect(replayed?.content).toBe("Let me check the file.\nThe file contains X."); }); + + it("keeps paired OpenAI tool call ids UTF-16 safe when truncating", () => { + const prefix = "a".repeat(39); + const oversizedId = `${prefix}🐱`; + const targetModel: Model<"openai-completions"> = { + ...model, + id: "target-model", + provider: "openai", + }; + const assistant: AssistantMessage = { + role: "assistant", + api: targetModel.api, + provider: targetModel.provider, + model: "source-model", + content: [{ type: "toolCall", id: oversizedId, name: "lookup", arguments: {} }], + usage: emptyUsage, + stopReason: "toolUse", + timestamp: 1, + }; + const context: Context = { + messages: [ + assistant, + { + role: "toolResult", + toolCallId: oversizedId, + toolName: "lookup", + content: [{ type: "text", text: "ok" }], + isError: false, + timestamp: 2, + }, + ], + }; + + const converted = convertMessages( + targetModel, + context, + resolveOpenAICompletionsCompat(targetModel), + ); + const assistantParam = converted.find((message) => message.role === "assistant"); + const toolParam = converted.find((message) => message.role === "tool"); + const normalizedAssistantId = + assistantParam?.role === "assistant" ? assistantParam.tool_calls?.[0]?.id : undefined; + const normalizedToolResultId = toolParam?.role === "tool" ? toolParam.tool_call_id : undefined; + + expect(oversizedId.slice(0, 40).charCodeAt(39)).toBe(0xd83d); + expect(normalizedAssistantId).toBe(prefix); + expect(normalizedToolResultId).toBe(prefix); + }); }); diff --git a/packages/ai/src/openai-completions-messages.ts b/packages/ai/src/openai-completions-messages.ts index 5a693a4278ce..9d4026657e6f 100644 --- a/packages/ai/src/openai-completions-messages.ts +++ b/packages/ai/src/openai-completions-messages.ts @@ -1,3 +1,4 @@ +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import type { ChatCompletionAssistantMessageParam, ChatCompletionContentPart, @@ -57,7 +58,7 @@ export function convertMessages( } if (model.provider === "openai") { - return id.length > 40 ? id.slice(0, 40) : id; + return id.length > 40 ? truncateUtf16Safe(id, 40) : id; } return id; };