Fix issue #4718: Enforce 40-char limit for OpenAI tool call IDs

OpenAI's API requires tool_call IDs to be <= 40 characters, but Clawdbot
was not enforcing this limit for OpenAI providers after commit 870bfa94e.

Changes:
- Re-enable tool call ID sanitization for OpenAI in transcript-policy.ts
- The sanitization preserves tool_use/tool_result pairing via stable mapping
- Add test case for 439-char ID (similar to reported 438-char issue)
- Update test expectations to reflect OpenAI now gets sanitized IDs

Root cause: Commit 870bfa94e disabled sanitization to "preserve Pi pairing",
but the sanitization logic already preserves pairing correctly via the
resolve() function in tool-call-id.ts.

Fixes openclaw/openclaw#4718
This commit is contained in:
spiceoogway 2026-01-30 10:56:36 -05:00
parent 56ee224d4c
commit ccac2aeec2
3 changed files with 42 additions and 4 deletions

View File

@ -94,7 +94,7 @@ describe("sanitizeSessionHistory", () => {
);
});
it("does not sanitize tool call ids for openai-responses", async () => {
it("sanitizes tool call ids for openai-responses to enforce 40-char limit", async () => {
vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false);
await sanitizeSessionHistory({
@ -108,7 +108,11 @@ describe("sanitizeSessionHistory", () => {
expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith(
mockMessages,
"session:history",
expect.objectContaining({ sanitizeMode: "images-only", sanitizeToolCallIds: false }),
expect.objectContaining({
sanitizeMode: "images-only",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
}),
);
});

View File

@ -137,6 +137,39 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
expect(r1.toolCallId).toBe(a.id);
expect(r2.toolCallId).toBe(b.id);
});
it("enforces 40-char limit for very long IDs (issue #4718)", () => {
// Simulate a very long tool call ID (like 438 chars reported in issue)
const veryLongId = `call_${"x".repeat(434)}`;
expect(veryLongId.length).toBe(439); // Verify we're testing a long ID
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: veryLongId, name: "gitlab", arguments: {} }],
},
{
role: "toolResult",
toolCallId: veryLongId,
toolName: "gitlab",
content: [{ type: "text", text: "ok" }],
},
] satisfies AgentMessage[];
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
expect(out).not.toBe(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
const toolCall = assistant.content?.[0] as { id?: string };
expect(toolCall.id).toBeDefined();
expect(toolCall.id?.length).toBeLessThanOrEqual(40);
expect(toolCall.id?.length).toBeGreaterThan(0);
expect(isValidCloudCodeAssistToolId(toolCall.id as string, "strict")).toBe(true);
const result = out[1] as Extract<AgentMessage, { role: "toolResult" }>;
expect(result.toolCallId).toBe(toolCall.id);
expect(result.toolCallId?.length).toBeLessThanOrEqual(40);
});
});
describe("strict mode (alphanumeric only)", () => {

View File

@ -85,7 +85,8 @@ export function resolveTranscriptPolicy(params: {
const needsNonImageSanitize = isGoogle || isAnthropic || isMistral || isOpenRouterGemini;
const sanitizeToolCallIds = isGoogle || isMistral;
// OpenAI requires tool call IDs <= 40 chars (issue #4718)
const sanitizeToolCallIds = isGoogle || isMistral || isOpenAi;
const toolCallIdMode: ToolCallIdMode | undefined = isMistral
? "strict9"
: sanitizeToolCallIds
@ -99,7 +100,7 @@ export function resolveTranscriptPolicy(params: {
return {
sanitizeMode: isOpenAi ? "images-only" : needsNonImageSanitize ? "full" : "images-only",
sanitizeToolCallIds: !isOpenAi && sanitizeToolCallIds,
sanitizeToolCallIds,
toolCallIdMode,
repairToolUseResultPairing: !isOpenAi && repairToolUseResultPairing,
preserveSignatures: isAntigravityClaudeModel,