mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: truncate call_id to 64 chars for OpenAI Responses API (#10763)
This commit is contained in:
parent
bbb6a6e4b4
commit
e34d93e2cb
4 changed files with 167 additions and 5 deletions
|
|
@ -23,6 +23,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { isMcpTool } from "../../utils/mcp-name"
|
||||
import { sanitizeOpenAiCallId } from "../../utils/tool-id"
|
||||
import { openAiCodexOAuthManager } from "../../integrations/openai-codex/oauth"
|
||||
import { t } from "../../i18n"
|
||||
|
||||
|
|
@ -426,7 +427,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
|
|||
: block.content?.map((c) => (c.type === "text" ? c.text : "")).join("") || ""
|
||||
toolResults.push({
|
||||
type: "function_call_output",
|
||||
call_id: block.tool_use_id,
|
||||
// Sanitize and truncate call_id to fit OpenAI's 64-char limit
|
||||
call_id: sanitizeOpenAiCallId(block.tool_use_id),
|
||||
output: result,
|
||||
})
|
||||
}
|
||||
|
|
@ -453,7 +455,8 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
|
|||
} else if (block.type === "tool_use") {
|
||||
toolCalls.push({
|
||||
type: "function_call",
|
||||
call_id: block.id,
|
||||
// Sanitize and truncate call_id to fit OpenAI's 64-char limit
|
||||
call_id: sanitizeOpenAiCallId(block.id),
|
||||
name: block.name,
|
||||
arguments: JSON.stringify(block.input),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { getModelParams } from "../transform/model-params"
|
|||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { isMcpTool } from "../../utils/mcp-name"
|
||||
import { sanitizeOpenAiCallId } from "../../utils/tool-id"
|
||||
|
||||
export type OpenAiNativeModel = ReturnType<OpenAiNativeHandler["getModel"]>
|
||||
|
||||
|
|
@ -486,7 +487,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
: block.content?.map((c) => (c.type === "text" ? c.text : "")).join("") || ""
|
||||
toolResults.push({
|
||||
type: "function_call_output",
|
||||
call_id: block.tool_use_id,
|
||||
// Sanitize and truncate call_id to fit OpenAI's 64-char limit
|
||||
call_id: sanitizeOpenAiCallId(block.tool_use_id),
|
||||
output: result,
|
||||
})
|
||||
}
|
||||
|
|
@ -516,7 +518,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
// Map Anthropic tool_use to Responses API function_call item
|
||||
toolCalls.push({
|
||||
type: "function_call",
|
||||
call_id: block.id,
|
||||
// Sanitize and truncate call_id to fit OpenAI's 64-char limit
|
||||
call_id: sanitizeOpenAiCallId(block.id),
|
||||
name: block.name,
|
||||
arguments: JSON.stringify(block.input),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { sanitizeToolUseId } from "../tool-id"
|
||||
import { sanitizeToolUseId, truncateOpenAiCallId, sanitizeOpenAiCallId, OPENAI_CALL_ID_MAX_LENGTH } from "../tool-id"
|
||||
|
||||
describe("sanitizeToolUseId", () => {
|
||||
describe("valid IDs pass through unchanged", () => {
|
||||
|
|
@ -69,3 +69,110 @@ describe("sanitizeToolUseId", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("truncateOpenAiCallId", () => {
|
||||
describe("IDs within limit pass through unchanged", () => {
|
||||
it("should preserve short IDs", () => {
|
||||
expect(truncateOpenAiCallId("toolu_01AbC")).toBe("toolu_01AbC")
|
||||
})
|
||||
|
||||
it("should preserve IDs exactly at the limit", () => {
|
||||
const id64Chars = "a".repeat(64)
|
||||
expect(truncateOpenAiCallId(id64Chars)).toBe(id64Chars)
|
||||
})
|
||||
|
||||
it("should handle empty string", () => {
|
||||
expect(truncateOpenAiCallId("")).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("long IDs get truncated with hash suffix", () => {
|
||||
it("should truncate IDs longer than 64 characters", () => {
|
||||
const longId = "a".repeat(70) // 70 chars, exceeds 64 limit
|
||||
const result = truncateOpenAiCallId(longId)
|
||||
expect(result.length).toBe(64)
|
||||
})
|
||||
|
||||
it("should produce consistent results for the same input", () => {
|
||||
const longId = "toolu_mcp--linear--create_issue_12345678-1234-1234-1234-123456789012"
|
||||
const result1 = truncateOpenAiCallId(longId)
|
||||
const result2 = truncateOpenAiCallId(longId)
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
|
||||
it("should produce different results for different inputs", () => {
|
||||
const longId1 = "a".repeat(70) + "_unique1"
|
||||
const longId2 = "a".repeat(70) + "_unique2"
|
||||
const result1 = truncateOpenAiCallId(longId1)
|
||||
const result2 = truncateOpenAiCallId(longId2)
|
||||
expect(result1).not.toBe(result2)
|
||||
})
|
||||
|
||||
it("should preserve the prefix and add hash suffix", () => {
|
||||
const longId = "toolu_mcp--linear--create_issue_" + "x".repeat(50)
|
||||
const result = truncateOpenAiCallId(longId)
|
||||
// Should start with the prefix (first 55 chars)
|
||||
expect(result.startsWith("toolu_mcp--linear--create_issue_")).toBe(true)
|
||||
// Should contain a separator and hash
|
||||
expect(result).toContain("_")
|
||||
})
|
||||
|
||||
it("should handle the exact reported issue length (69 chars)", () => {
|
||||
// The original error mentioned 69 characters
|
||||
const id69Chars = "toolu_mcp--posthog--query_run_" + "a".repeat(39) // total 69 chars
|
||||
expect(id69Chars.length).toBe(69)
|
||||
const result = truncateOpenAiCallId(id69Chars)
|
||||
expect(result.length).toBe(64)
|
||||
})
|
||||
})
|
||||
|
||||
describe("custom max length", () => {
|
||||
it("should support custom max length", () => {
|
||||
const longId = "a".repeat(50)
|
||||
const result = truncateOpenAiCallId(longId, 32)
|
||||
expect(result.length).toBe(32)
|
||||
})
|
||||
|
||||
it("should not truncate if within custom limit", () => {
|
||||
const id = "short_id"
|
||||
expect(truncateOpenAiCallId(id, 100)).toBe(id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("sanitizeOpenAiCallId", () => {
|
||||
it("should sanitize characters and truncate if needed", () => {
|
||||
// ID with invalid chars and too long
|
||||
const longIdWithInvalidChars = "toolu_mcp.server:tool/name_" + "x".repeat(50)
|
||||
const result = sanitizeOpenAiCallId(longIdWithInvalidChars)
|
||||
// Should be within limit
|
||||
expect(result.length).toBeLessThanOrEqual(64)
|
||||
// Should not contain invalid characters
|
||||
expect(result).toMatch(/^[a-zA-Z0-9_-]+$/)
|
||||
})
|
||||
|
||||
it("should only sanitize if length is within limit", () => {
|
||||
const shortIdWithInvalidChars = "tool.with.dots"
|
||||
const result = sanitizeOpenAiCallId(shortIdWithInvalidChars)
|
||||
expect(result).toBe("tool_with_dots")
|
||||
})
|
||||
|
||||
it("should handle real-world MCP tool IDs", () => {
|
||||
// Real MCP tool ID that might exceed 64 chars
|
||||
const mcpToolId = "call_mcp--posthog--dashboard_create_12345678-1234-1234-1234-123456789012"
|
||||
const result = sanitizeOpenAiCallId(mcpToolId)
|
||||
expect(result.length).toBeLessThanOrEqual(64)
|
||||
expect(result).toMatch(/^[a-zA-Z0-9_-]+$/)
|
||||
})
|
||||
|
||||
it("should preserve IDs that are already valid and within limit", () => {
|
||||
const validId = "toolu_01AbC-xyz_789"
|
||||
expect(sanitizeOpenAiCallId(validId)).toBe(validId)
|
||||
})
|
||||
})
|
||||
|
||||
describe("OPENAI_CALL_ID_MAX_LENGTH constant", () => {
|
||||
it("should be 64", () => {
|
||||
expect(OPENAI_CALL_ID_MAX_LENGTH).toBe(64)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
import * as crypto from "crypto"
|
||||
|
||||
/**
|
||||
* OpenAI Responses API maximum length for call_id field.
|
||||
* This limit applies to both function_call and function_call_output items.
|
||||
*/
|
||||
export const OPENAI_CALL_ID_MAX_LENGTH = 64
|
||||
|
||||
/**
|
||||
* Sanitize a tool_use ID to match API validation pattern: ^[a-zA-Z0-9_-]+$
|
||||
* Replaces any invalid character with underscore.
|
||||
|
|
@ -5,3 +13,44 @@
|
|||
export function sanitizeToolUseId(id: string): string {
|
||||
return id.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a call_id to fit within OpenAI's 64-character limit.
|
||||
* Uses a hash suffix to maintain uniqueness when truncation is needed.
|
||||
*
|
||||
* @param id - The original call_id
|
||||
* @param maxLength - Maximum length (defaults to OpenAI's 64-char limit)
|
||||
* @returns The truncated ID, or original if already within limits
|
||||
*/
|
||||
export function truncateOpenAiCallId(id: string, maxLength: number = OPENAI_CALL_ID_MAX_LENGTH): string {
|
||||
if (id.length <= maxLength) {
|
||||
return id
|
||||
}
|
||||
|
||||
// Use 8-char hash suffix for uniqueness (from MD5, sufficient for collision resistance in this context)
|
||||
const hashSuffixLength = 8
|
||||
const separator = "_"
|
||||
// Reserve space for separator + hash
|
||||
const prefixMaxLength = maxLength - separator.length - hashSuffixLength
|
||||
|
||||
// Create hash of the full original ID for uniqueness
|
||||
const hash = crypto.createHash("md5").update(id).digest("hex").slice(0, hashSuffixLength)
|
||||
|
||||
// Take the prefix and append hash
|
||||
const prefix = id.slice(0, prefixMaxLength)
|
||||
return `${prefix}${separator}${hash}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and truncate a tool call ID for OpenAI's Responses API.
|
||||
* This combines character sanitization with length truncation.
|
||||
*
|
||||
* @param id - The original call_id
|
||||
* @param maxLength - Maximum length (defaults to OpenAI's 64-char limit)
|
||||
* @returns The sanitized and truncated ID
|
||||
*/
|
||||
export function sanitizeOpenAiCallId(id: string, maxLength: number = OPENAI_CALL_ID_MAX_LENGTH): string {
|
||||
// First sanitize characters, then truncate
|
||||
const sanitized = sanitizeToolUseId(id)
|
||||
return truncateOpenAiCallId(sanitized, maxLength)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue