fix(ai): normalize pipe-separated tool call IDs for cross-provider handoff

- Handle pipe-separated IDs from OpenAI Responses API in openai-completions provider
- Strip trailing underscores after truncation in openai-responses-shared (OpenAI Codex rejects them)
- Add regression tests for tool call ID normalization

fixes #1022
This commit is contained in:
Mario Zechner 2026-01-29 01:28:12 +01:00
parent 4f004adefa
commit 605f6f494b
4 changed files with 308 additions and 2 deletions

View file

@ -497,6 +497,17 @@ export function convertMessages(
const normalizeToolCallId = (id: string): string => {
if (compat.requiresMistralToolIds) return normalizeMistralToolId(id);
// Handle pipe-separated IDs from OpenAI Responses API
// Format: {call_id}|{id} where {id} can be 400+ chars with special chars (+, /, =)
// These come from providers like github-copilot, openai-codex, opencode
// Extract just the call_id part and normalize it
if (id.includes("|")) {
const [callId] = id.split("|");
// Sanitize to allowed chars and truncate to 40 chars (OpenAI limit)
return callId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40);
}
if (model.provider === "openai") return id.length > 40 ? id.slice(0, 40) : id;
// Copilot Claude models route to Claude backend which requires Anthropic ID format
if (model.provider === "github-copilot" && model.id.toLowerCase().includes("claude")) {

View file

@ -86,8 +86,11 @@ export function convertResponsesMessages<TApi extends Api>(
if (!sanitizedItemId.startsWith("fc")) {
sanitizedItemId = `fc_${sanitizedItemId}`;
}
const normalizedCallId = sanitizedCallId.length > 64 ? sanitizedCallId.slice(0, 64) : sanitizedCallId;
const normalizedItemId = sanitizedItemId.length > 64 ? sanitizedItemId.slice(0, 64) : sanitizedItemId;
// Truncate to 64 chars and strip trailing underscores (OpenAI Codex rejects them)
let normalizedCallId = sanitizedCallId.length > 64 ? sanitizedCallId.slice(0, 64) : sanitizedCallId;
let normalizedItemId = sanitizedItemId.length > 64 ? sanitizedItemId.slice(0, 64) : sanitizedItemId;
normalizedCallId = normalizedCallId.replace(/_+$/, "");
normalizedItemId = normalizedItemId.replace(/_+$/, "");
return `${normalizedCallId}|${normalizedItemId}`;
};