Skip to content

Commit ed9ae04

Browse files
authored
Fix Azure 40 character limit on tool_call_id (#5575)
1 parent 227d720 commit ed9ae04

File tree

2 files changed

+74
-2
lines changed

2 files changed

+74
-2
lines changed

packages/__tests__/llm-mapper/openai-chat-to-responses-converters.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,51 @@ describe("OpenAI Chat -> Responses converters", () => {
373373
expect(assistant2.tool_calls?.[0].id).toBe("call_c");
374374
});
375375

376+
it("truncates long tool_call_ids to 40 characters for Azure compatibility", () => {
377+
const longId = "call_1234567890abcdefghij1234567890abcdefghij1234567890"; // 54 chars
378+
const req = {
379+
model: "gpt-4o-mini",
380+
input: [
381+
{ role: "user", content: "Hello" },
382+
{ type: "function_call", call_id: longId, name: "my_func", arguments: "{}" },
383+
{ type: "function_call_output", call_id: longId, output: "result" },
384+
],
385+
} as unknown as ResponsesRequestBody;
386+
387+
const oai = toChatCompletions(req);
388+
389+
// Check assistant message tool_call id is truncated
390+
const assistant = oai.messages?.[1] as any;
391+
expect(assistant.tool_calls?.[0].id.length).toBeLessThanOrEqual(40);
392+
393+
// Check tool response tool_call_id is truncated
394+
const toolResponse = oai.messages?.[2] as any;
395+
expect(toolResponse.tool_call_id.length).toBeLessThanOrEqual(40);
396+
397+
// Both should have the same truncated ID (deterministic)
398+
expect(assistant.tool_calls?.[0].id).toBe(toolResponse.tool_call_id);
399+
});
400+
401+
it("preserves short tool_call_ids unchanged", () => {
402+
const shortId = "call_abc123"; // < 40 chars
403+
const req = {
404+
model: "gpt-4o-mini",
405+
input: [
406+
{ role: "user", content: "Hello" },
407+
{ type: "function_call", call_id: shortId, name: "my_func", arguments: "{}" },
408+
{ type: "function_call_output", call_id: shortId, output: "result" },
409+
],
410+
} as unknown as ResponsesRequestBody;
411+
412+
const oai = toChatCompletions(req);
413+
414+
const assistant = oai.messages?.[1] as any;
415+
expect(assistant.tool_calls?.[0].id).toBe(shortId);
416+
417+
const toolResponse = oai.messages?.[2] as any;
418+
expect(toolResponse.tool_call_id).toBe(shortId);
419+
});
420+
376421
it("maps Responses tools (flattened) to Chat tools (nested)", () => {
377422
const req = {
378423
model: "gpt-4o-mini",

packages/llm-mapper/transform/providers/responses/request/toChatCompletions.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,33 @@ import {
1111
} from "@helicone-package/prompts/types";
1212
import { ResponsesToolDefinition } from "../../../types/responses";
1313

14+
/**
15+
* Azure has a 40 character limit on tool_call_id.
16+
* This function truncates long IDs deterministically so that:
17+
* 1. IDs <= 40 chars are unchanged
18+
* 2. IDs > 40 chars are shortened to prefix + hash suffix
19+
* The same input always produces the same output, ensuring tool_calls
20+
* and their corresponding tool responses match.
21+
*/
22+
const AZURE_TOOL_CALL_ID_LIMIT = 40;
23+
24+
function truncateToolCallId(id: string): string {
25+
if (id.length <= AZURE_TOOL_CALL_ID_LIMIT) {
26+
return id;
27+
}
28+
// Use a simple deterministic hash for the suffix
29+
let hash = 0;
30+
for (let i = 0; i < id.length; i++) {
31+
const char = id.charCodeAt(i);
32+
hash = ((hash << 5) - hash) + char;
33+
hash = hash & hash; // Convert to 32-bit integer
34+
}
35+
const hashStr = Math.abs(hash).toString(36);
36+
// Keep prefix + underscore + hash, ensuring total <= 40 chars
37+
const prefixLength = AZURE_TOOL_CALL_ID_LIMIT - hashStr.length - 1;
38+
return `${id.substring(0, prefixLength)}_${hashStr}`;
39+
}
40+
1441
function mapRole(role: string): "system" | "user" | "assistant" | "tool" | "function" {
1542
if (role === "developer") return "system";
1643
if (role === "system" || role === "user" || role === "assistant") return role;
@@ -88,7 +115,7 @@ function convertInputToMessages(input: ResponsesRequestBody["input"]) {
88115
>(input, i, "function_call");
89116

90117
const toolCalls = functionCalls.map((fc, idx) => ({
91-
id: fc.id || fc.call_id || `call_${i + idx}`,
118+
id: truncateToolCallId(fc.id || fc.call_id || `call_${i + idx}`),
92119
type: "function" as const,
93120
function: {
94121
name: fc.name,
@@ -110,7 +137,7 @@ function convertInputToMessages(input: ResponsesRequestBody["input"]) {
110137
const fco = item;
111138
messages.push({
112139
role: "tool",
113-
tool_call_id: fco.call_id,
140+
tool_call_id: truncateToolCallId(fco.call_id),
114141
content: fco.output ?? "",
115142
});
116143
continue;

0 commit comments

Comments
 (0)