mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-22 01:02:16 +00:00
fix(ai): Sanitize tool call IDs for Anthropic API compatibility
- Anthropic API requires tool call IDs to match pattern ^[a-zA-Z0-9_-]+$ - OpenAI Responses API generates IDs with pipe character (|) which breaks Anthropic - Added sanitizeToolCallId() to replace invalid characters with underscores - Fixes cross-provider handoffs from OpenAI Responses to Anthropic - Added test to verify the fix works
This commit is contained in:
parent
080bf67306
commit
6679a83b32
3 changed files with 180 additions and 61 deletions
|
|
@ -2610,13 +2610,13 @@ export const MODELS = {
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.003,
|
input: 0.012,
|
||||||
output: 0.006,
|
output: 0.024,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 20000,
|
contextWindow: 131072,
|
||||||
maxTokens: 20000,
|
maxTokens: 16384,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"qwen/qwen-2.5-72b-instruct": {
|
"qwen/qwen-2.5-72b-instruct": {
|
||||||
id: "qwen/qwen-2.5-72b-instruct",
|
id: "qwen/qwen-2.5-72b-instruct",
|
||||||
|
|
@ -2652,23 +2652,6 @@ export const MODELS = {
|
||||||
contextWindow: 32768,
|
contextWindow: 32768,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"cohere/command-r-plus-08-2024": {
|
|
||||||
id: "cohere/command-r-plus-08-2024",
|
|
||||||
name: "Cohere: Command R+ (08-2024)",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: {
|
|
||||||
input: 2.5,
|
|
||||||
output: 10,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
},
|
|
||||||
contextWindow: 128000,
|
|
||||||
maxTokens: 4000,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"cohere/command-r-08-2024": {
|
"cohere/command-r-08-2024": {
|
||||||
id: "cohere/command-r-08-2024",
|
id: "cohere/command-r-08-2024",
|
||||||
name: "Cohere: Command R (08-2024)",
|
name: "Cohere: Command R (08-2024)",
|
||||||
|
|
@ -2686,6 +2669,23 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 4000,
|
maxTokens: 4000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"cohere/command-r-plus-08-2024": {
|
||||||
|
id: "cohere/command-r-plus-08-2024",
|
||||||
|
name: "Cohere: Command R+ (08-2024)",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 2.5,
|
||||||
|
output: 10,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 4000,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"microsoft/phi-3.5-mini-128k-instruct": {
|
"microsoft/phi-3.5-mini-128k-instruct": {
|
||||||
id: "microsoft/phi-3.5-mini-128k-instruct",
|
id: "microsoft/phi-3.5-mini-128k-instruct",
|
||||||
name: "Microsoft: Phi-3.5 Mini 128K Instruct",
|
name: "Microsoft: Phi-3.5 Mini 128K Instruct",
|
||||||
|
|
@ -2720,23 +2720,6 @@ export const MODELS = {
|
||||||
contextWindow: 131072,
|
contextWindow: 131072,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"meta-llama/llama-3.1-8b-instruct": {
|
|
||||||
id: "meta-llama/llama-3.1-8b-instruct",
|
|
||||||
name: "Meta: Llama 3.1 8B Instruct",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: {
|
|
||||||
input: 0.015,
|
|
||||||
output: 0.02,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
},
|
|
||||||
contextWindow: 131072,
|
|
||||||
maxTokens: 16384,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"meta-llama/llama-3.1-405b-instruct": {
|
"meta-llama/llama-3.1-405b-instruct": {
|
||||||
id: "meta-llama/llama-3.1-405b-instruct",
|
id: "meta-llama/llama-3.1-405b-instruct",
|
||||||
name: "Meta: Llama 3.1 405B Instruct",
|
name: "Meta: Llama 3.1 405B Instruct",
|
||||||
|
|
@ -2754,6 +2737,23 @@ export const MODELS = {
|
||||||
contextWindow: 32768,
|
contextWindow: 32768,
|
||||||
maxTokens: 16384,
|
maxTokens: 16384,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"meta-llama/llama-3.1-8b-instruct": {
|
||||||
|
id: "meta-llama/llama-3.1-8b-instruct",
|
||||||
|
name: "Meta: Llama 3.1 8B Instruct",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.015,
|
||||||
|
output: 0.02,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 131072,
|
||||||
|
maxTokens: 16384,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"meta-llama/llama-3.1-70b-instruct": {
|
"meta-llama/llama-3.1-70b-instruct": {
|
||||||
id: "meta-llama/llama-3.1-70b-instruct",
|
id: "meta-llama/llama-3.1-70b-instruct",
|
||||||
name: "Meta: Llama 3.1 70B Instruct",
|
name: "Meta: Llama 3.1 70B Instruct",
|
||||||
|
|
@ -2780,13 +2780,13 @@ export const MODELS = {
|
||||||
reasoning: false,
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.0075,
|
input: 0.01,
|
||||||
output: 0.049999999999999996,
|
output: 0.0400032,
|
||||||
cacheRead: 0,
|
cacheRead: 0,
|
||||||
cacheWrite: 0,
|
cacheWrite: 0,
|
||||||
},
|
},
|
||||||
contextWindow: 32000,
|
contextWindow: 131072,
|
||||||
maxTokens: 4096,
|
maxTokens: 128000,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"mistralai/mistral-7b-instruct:free": {
|
"mistralai/mistral-7b-instruct:free": {
|
||||||
id: "mistralai/mistral-7b-instruct:free",
|
id: "mistralai/mistral-7b-instruct:free",
|
||||||
|
|
@ -2873,23 +2873,6 @@ export const MODELS = {
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
"meta-llama/llama-3-8b-instruct": {
|
|
||||||
id: "meta-llama/llama-3-8b-instruct",
|
|
||||||
name: "Meta: Llama 3 8B Instruct",
|
|
||||||
api: "openai-completions",
|
|
||||||
provider: "openrouter",
|
|
||||||
baseUrl: "https://openrouter.ai/api/v1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: {
|
|
||||||
input: 0.03,
|
|
||||||
output: 0.06,
|
|
||||||
cacheRead: 0,
|
|
||||||
cacheWrite: 0,
|
|
||||||
},
|
|
||||||
contextWindow: 8192,
|
|
||||||
maxTokens: 16384,
|
|
||||||
} satisfies Model<"openai-completions">,
|
|
||||||
"meta-llama/llama-3-70b-instruct": {
|
"meta-llama/llama-3-70b-instruct": {
|
||||||
id: "meta-llama/llama-3-70b-instruct",
|
id: "meta-llama/llama-3-70b-instruct",
|
||||||
name: "Meta: Llama 3 70B Instruct",
|
name: "Meta: Llama 3 70B Instruct",
|
||||||
|
|
@ -2907,6 +2890,23 @@ export const MODELS = {
|
||||||
contextWindow: 8192,
|
contextWindow: 8192,
|
||||||
maxTokens: 16384,
|
maxTokens: 16384,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
|
"meta-llama/llama-3-8b-instruct": {
|
||||||
|
id: "meta-llama/llama-3-8b-instruct",
|
||||||
|
name: "Meta: Llama 3 8B Instruct",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "openrouter",
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.03,
|
||||||
|
output: 0.06,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 8192,
|
||||||
|
maxTokens: 16384,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
"mistralai/mixtral-8x22b-instruct": {
|
"mistralai/mixtral-8x22b-instruct": {
|
||||||
id: "mistralai/mixtral-8x22b-instruct",
|
id: "mistralai/mixtral-8x22b-instruct",
|
||||||
name: "Mistral: Mixtral 8x22B Instruct",
|
name: "Mistral: Mixtral 8x22B Instruct",
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,12 @@ function buildParams(
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize tool call IDs to match Anthropic's required pattern: ^[a-zA-Z0-9_-]+$
|
||||||
|
function sanitizeToolCallId(id: string): string {
|
||||||
|
// Replace any character that isn't alphanumeric, underscore, or hyphen with underscore
|
||||||
|
return id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
function convertMessages(messages: Message[], model: Model<"anthropic-messages">): MessageParam[] {
|
function convertMessages(messages: Message[], model: Model<"anthropic-messages">): MessageParam[] {
|
||||||
const params: MessageParam[] = [];
|
const params: MessageParam[] = [];
|
||||||
|
|
||||||
|
|
@ -348,7 +354,7 @@ function convertMessages(messages: Message[], model: Model<"anthropic-messages">
|
||||||
} else if (block.type === "toolCall") {
|
} else if (block.type === "toolCall") {
|
||||||
blocks.push({
|
blocks.push({
|
||||||
type: "tool_use",
|
type: "tool_use",
|
||||||
id: block.id,
|
id: sanitizeToolCallId(block.id),
|
||||||
name: block.name,
|
name: block.name,
|
||||||
input: block.arguments,
|
input: block.arguments,
|
||||||
});
|
});
|
||||||
|
|
@ -365,7 +371,7 @@ function convertMessages(messages: Message[], model: Model<"anthropic-messages">
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "tool_result",
|
type: "tool_result",
|
||||||
tool_use_id: msg.toolCallId,
|
tool_use_id: sanitizeToolCallId(msg.toolCallId),
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
is_error: msg.isError,
|
is_error: msg.isError,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
113
packages/ai/test/cross-provider-toolcall.test.ts
Normal file
113
packages/ai/test/cross-provider-toolcall.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { type Context, complete, getModel } from "../src/index.js";
|
||||||
|
|
||||||
|
async function testCrossProviderToolCall() {
|
||||||
|
console.log("Testing cross-provider tool call handoff...\n");
|
||||||
|
|
||||||
|
// Define a simple tool
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
name: "get_weather",
|
||||||
|
description: "Get current weather for a location",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
location: { type: "string", description: "City name" },
|
||||||
|
},
|
||||||
|
required: ["location"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create context with tools
|
||||||
|
const context: Context = {
|
||||||
|
systemPrompt: "You are a helpful assistant. Use the get_weather tool when asked about weather.",
|
||||||
|
messages: [{ role: "user", content: "What is the weather in Paris?" }],
|
||||||
|
tools,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Get tool call from GPT-5
|
||||||
|
console.log("Step 1: Getting tool call from GPT-5...");
|
||||||
|
const gpt5 = getModel("openai", "gpt-5-mini");
|
||||||
|
const gpt5Response = await complete(gpt5, context);
|
||||||
|
context.messages.push(gpt5Response);
|
||||||
|
|
||||||
|
// Check for tool calls
|
||||||
|
const toolCalls = gpt5Response.content.filter((b) => b.type === "toolCall");
|
||||||
|
console.log(`GPT-5 made ${toolCalls.length} tool call(s)`);
|
||||||
|
|
||||||
|
if (toolCalls.length > 0) {
|
||||||
|
const toolCall = toolCalls[0];
|
||||||
|
console.log(`Tool call ID: ${toolCall.id}`);
|
||||||
|
console.log(`Tool call contains pipe: ${toolCall.id.includes("|")}`);
|
||||||
|
console.log(`Tool: ${toolCall.name}(${JSON.stringify(toolCall.arguments)})\n`);
|
||||||
|
|
||||||
|
// Add tool result
|
||||||
|
context.messages.push({
|
||||||
|
role: "toolResult",
|
||||||
|
toolCallId: toolCall.id,
|
||||||
|
toolName: toolCall.name,
|
||||||
|
content: JSON.stringify({
|
||||||
|
location: "Paris",
|
||||||
|
temperature: "22°C",
|
||||||
|
conditions: "Partly cloudy",
|
||||||
|
}),
|
||||||
|
isError: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Send to Claude Haiku for follow-up
|
||||||
|
console.log("Step 2: Sending to Claude Haiku for follow-up...");
|
||||||
|
const haiku = getModel("anthropic", "claude-3-5-haiku-20241022");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const haikuResponse = await complete(haiku, context);
|
||||||
|
console.log("✅ Claude Haiku successfully processed the conversation!");
|
||||||
|
console.log("Response content types:", haikuResponse.content.map((b) => b.type).join(", "));
|
||||||
|
console.log("Number of content blocks:", haikuResponse.content.length);
|
||||||
|
console.log("Stop reason:", haikuResponse.stopReason);
|
||||||
|
if (haikuResponse.error) {
|
||||||
|
console.log("Error message:", haikuResponse.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print all response content
|
||||||
|
for (const block of haikuResponse.content) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
console.log("\nClaude text response:", block.text);
|
||||||
|
} else if (block.type === "thinking") {
|
||||||
|
console.log("\nClaude thinking:", block.thinking);
|
||||||
|
} else if (block.type === "toolCall") {
|
||||||
|
console.log("\nClaude tool call:", block.name, block.arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (haikuResponse.content.length === 0) {
|
||||||
|
console.log("⚠️ Claude returned an empty response!");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Claude Haiku failed to process the conversation:");
|
||||||
|
console.error("Error:", error);
|
||||||
|
|
||||||
|
// Check if it's related to the tool call ID
|
||||||
|
if (error instanceof Error && error.message.includes("tool")) {
|
||||||
|
console.error("\n⚠️ This appears to be a tool call ID issue!");
|
||||||
|
console.error("The pipe character (|) in OpenAI Response API tool IDs might be causing problems.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No tool calls were made by GPT-5");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Test failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set API keys from environment or pass them explicitly
|
||||||
|
const openaiKey = process.env.OPENAI_API_KEY;
|
||||||
|
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
|
if (!openaiKey || !anthropicKey) {
|
||||||
|
console.error("Please set OPENAI_API_KEY and ANTHROPIC_API_KEY environment variables");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
testCrossProviderToolCall().catch(console.error);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue