mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 17:00:58 +00:00
Complete the remaining pi-to-companion rename across companion-os, web, vm-orchestrator, docker, and archived fixtures. Verification: - semantic rg sweeps for Pi/piConfig/getPi/.pi runtime references - npm run check in apps/companion-os (fails in this worktree: biome not found) Co-authored-by: Codex <noreply@openai.com>
320 lines
9.7 KiB
TypeScript
320 lines
9.7 KiB
TypeScript
/**
|
|
* Tool Call ID Normalization Tests
|
|
*
|
|
* Tests that tool call IDs from OpenAI Responses API (github-copilot, openai-codex, opencode)
|
|
* are properly normalized when sent to other providers.
|
|
*
|
|
* OpenAI Responses API generates IDs in format: {call_id}|{id}
|
|
* where {id} can be 400+ chars with special characters (+, /, =).
|
|
*
|
|
* Regression test for: https://github.com/badlogic/companion-mono/issues/1022
|
|
*/
|
|
|
|
import { Type } from "@sinclair/typebox";
|
|
import { describe, expect, it } from "vitest";
|
|
import { getModel } from "../src/models.js";
|
|
import { completeSimple, getEnvApiKey } from "../src/stream.js";
|
|
import type {
|
|
AssistantMessage,
|
|
Message,
|
|
Tool,
|
|
ToolResultMessage,
|
|
} from "../src/types.js";
|
|
import { resolveApiKey } from "./oauth.js";
|
|
|
|
// Resolve API keys
|
|
const copilotToken = await resolveApiKey("github-copilot");
|
|
const openrouterKey = getEnvApiKey("openrouter");
|
|
const codexToken = await resolveApiKey("openai-codex");
|
|
|
|
// Simple echo tool for testing
|
|
const echoToolSchema = Type.Object({
|
|
message: Type.String({ description: "Message to echo back" }),
|
|
});
|
|
|
|
const echoTool: Tool<typeof echoToolSchema> = {
|
|
name: "echo",
|
|
description: "Echoes the message back",
|
|
parameters: echoToolSchema,
|
|
};
|
|
|
|
/**
|
|
* Test 1: Live cross-provider handoff
|
|
*
|
|
* 1. Use github-copilot gpt-5.2-codex to generate a tool call
|
|
* 2. Switch to openrouter openai/gpt-5.2-codex and complete
|
|
* 3. Switch to openai-codex gpt-5.2-codex and complete
|
|
*
|
|
* Both should succeed without "call_id too long" errors.
|
|
*/
|
|
describe("Tool Call ID Normalization - Live Handoff", () => {
|
|
it.skipIf(!copilotToken || !openrouterKey)(
|
|
"github-copilot -> openrouter should normalize pipe-separated IDs",
|
|
async () => {
|
|
const copilotModel = getModel("github-copilot", "gpt-5.2-codex");
|
|
const openrouterModel = getModel("openrouter", "openai/gpt-5.2-codex");
|
|
|
|
// Step 1: Generate tool call with github-copilot
|
|
const userMessage: Message = {
|
|
role: "user",
|
|
content: "Use the echo tool to echo 'hello world'",
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
const assistantResponse = await completeSimple(
|
|
copilotModel,
|
|
{
|
|
systemPrompt:
|
|
"You are a helpful assistant. Use the echo tool when asked.",
|
|
messages: [userMessage],
|
|
tools: [echoTool],
|
|
},
|
|
{ apiKey: copilotToken },
|
|
);
|
|
|
|
expect(
|
|
assistantResponse.stopReason,
|
|
`Copilot error: ${assistantResponse.errorMessage}`,
|
|
).toBe("toolUse");
|
|
|
|
const toolCall = assistantResponse.content.find(
|
|
(c) => c.type === "toolCall",
|
|
);
|
|
expect(toolCall).toBeDefined();
|
|
expect(toolCall!.type).toBe("toolCall");
|
|
|
|
// Verify it's a pipe-separated ID (OpenAI Responses format)
|
|
if (toolCall?.type === "toolCall") {
|
|
expect(toolCall.id).toContain("|");
|
|
console.log(
|
|
`Tool call ID from github-copilot: ${toolCall.id.slice(0, 80)}...`,
|
|
);
|
|
}
|
|
|
|
// Create tool result
|
|
const toolResult: ToolResultMessage = {
|
|
role: "toolResult",
|
|
toolCallId: (toolCall as any).id,
|
|
toolName: "echo",
|
|
content: [{ type: "text", text: "hello world" }],
|
|
isError: false,
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
// Step 2: Complete with openrouter (uses openai-completions API)
|
|
const openrouterResponse = await completeSimple(
|
|
openrouterModel,
|
|
{
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [
|
|
userMessage,
|
|
assistantResponse,
|
|
toolResult,
|
|
{ role: "user", content: "Say hi", timestamp: Date.now() },
|
|
],
|
|
tools: [echoTool],
|
|
},
|
|
{ apiKey: openrouterKey },
|
|
);
|
|
|
|
// Should NOT fail with "call_id too long" error
|
|
expect(
|
|
openrouterResponse.stopReason,
|
|
`OpenRouter error: ${openrouterResponse.errorMessage}`,
|
|
).not.toBe("error");
|
|
expect(openrouterResponse.errorMessage).toBeUndefined();
|
|
},
|
|
60000,
|
|
);
|
|
|
|
it.skipIf(!copilotToken || !codexToken)(
|
|
"github-copilot -> openai-codex should normalize pipe-separated IDs",
|
|
async () => {
|
|
const copilotModel = getModel("github-copilot", "gpt-5.2-codex");
|
|
const codexModel = getModel("openai-codex", "gpt-5.2-codex");
|
|
|
|
// Step 1: Generate tool call with github-copilot
|
|
const userMessage: Message = {
|
|
role: "user",
|
|
content: "Use the echo tool to echo 'test message'",
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
const assistantResponse = await completeSimple(
|
|
copilotModel,
|
|
{
|
|
systemPrompt:
|
|
"You are a helpful assistant. Use the echo tool when asked.",
|
|
messages: [userMessage],
|
|
tools: [echoTool],
|
|
},
|
|
{ apiKey: copilotToken },
|
|
);
|
|
|
|
expect(
|
|
assistantResponse.stopReason,
|
|
`Copilot error: ${assistantResponse.errorMessage}`,
|
|
).toBe("toolUse");
|
|
|
|
const toolCall = assistantResponse.content.find(
|
|
(c) => c.type === "toolCall",
|
|
);
|
|
expect(toolCall).toBeDefined();
|
|
|
|
// Create tool result
|
|
const toolResult: ToolResultMessage = {
|
|
role: "toolResult",
|
|
toolCallId: (toolCall as any).id,
|
|
toolName: "echo",
|
|
content: [{ type: "text", text: "test message" }],
|
|
isError: false,
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
// Step 2: Complete with openai-codex (uses openai-codex-responses API)
|
|
const codexResponse = await completeSimple(
|
|
codexModel,
|
|
{
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [
|
|
userMessage,
|
|
assistantResponse,
|
|
toolResult,
|
|
{ role: "user", content: "Say hi", timestamp: Date.now() },
|
|
],
|
|
tools: [echoTool],
|
|
},
|
|
{ apiKey: codexToken },
|
|
);
|
|
|
|
// Should NOT fail with ID validation error
|
|
expect(
|
|
codexResponse.stopReason,
|
|
`Codex error: ${codexResponse.errorMessage}`,
|
|
).not.toBe("error");
|
|
expect(codexResponse.errorMessage).toBeUndefined();
|
|
},
|
|
60000,
|
|
);
|
|
});
|
|
|
|
/**
|
|
* Test 2: Prefilled context with exact failing IDs from issue #1022
|
|
*
|
|
* Uses the exact tool call ID format that caused the error:
|
|
* "call_xxx|very_long_base64_with_special_chars+/="
|
|
*/
|
|
describe("Tool Call ID Normalization - Prefilled Context", () => {
|
|
// Exact tool call ID from issue #1022 JSONL
|
|
const FAILING_TOOL_CALL_ID =
|
|
"call_pAYbIr76hXIjncD9UE4eGfnS|t5nnb2qYMFWGSsr13fhCd1CaCu3t3qONEPuOudu4HSVEtA8YJSL6FAZUxvoOoD792VIJWl91g87EdqsCWp9krVsdBysQoDaf9lMCLb8BS4EYi4gQd5kBQBYLlgD71PYwvf+TbMD9J9/5OMD42oxSRj8H+vRf78/l2Xla33LWz4nOgsddBlbvabICRs8GHt5C9PK5keFtzyi3lsyVKNlfduK3iphsZqs4MLv4zyGJnvZo/+QzShyk5xnMSQX/f98+aEoNflEApCdEOXipipgeiNWnpFSHbcwmMkZoJhURNu+JEz3xCh1mrXeYoN5o+trLL3IXJacSsLYXDrYTipZZbJFRPAucgbnjYBC+/ZzJOfkwCs+Gkw7EoZR7ZQgJ8ma+9586n4tT4cI8DEhBSZsWMjrCt8dxKg==";
|
|
|
|
// Build prefilled context with the failing ID
|
|
function buildPrefilledMessages(): Message[] {
|
|
const userMessage: Message = {
|
|
role: "user",
|
|
content: "Use the echo tool to echo 'hello'",
|
|
timestamp: Date.now() - 2000,
|
|
};
|
|
|
|
const assistantMessage: AssistantMessage = {
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "toolCall",
|
|
id: FAILING_TOOL_CALL_ID,
|
|
name: "echo",
|
|
arguments: { message: "hello" },
|
|
},
|
|
],
|
|
api: "openai-responses",
|
|
provider: "github-copilot",
|
|
model: "gpt-5.2-codex",
|
|
usage: {
|
|
input: 100,
|
|
output: 50,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 150,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
stopReason: "toolUse",
|
|
timestamp: Date.now() - 1500,
|
|
};
|
|
|
|
const toolResult: ToolResultMessage = {
|
|
role: "toolResult",
|
|
toolCallId: FAILING_TOOL_CALL_ID,
|
|
toolName: "echo",
|
|
content: [{ type: "text", text: "hello" }],
|
|
isError: false,
|
|
timestamp: Date.now() - 1000,
|
|
};
|
|
|
|
const followUpUser: Message = {
|
|
role: "user",
|
|
content: "Say hi",
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
return [userMessage, assistantMessage, toolResult, followUpUser];
|
|
}
|
|
|
|
it.skipIf(!openrouterKey)(
|
|
"openrouter should handle prefilled context with long pipe-separated IDs",
|
|
async () => {
|
|
const model = getModel("openrouter", "openai/gpt-5.2-codex");
|
|
const messages = buildPrefilledMessages();
|
|
|
|
const response = await completeSimple(
|
|
model,
|
|
{
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages,
|
|
tools: [echoTool],
|
|
},
|
|
{ apiKey: openrouterKey },
|
|
);
|
|
|
|
// Should NOT fail with "call_id too long" error
|
|
expect(
|
|
response.stopReason,
|
|
`OpenRouter error: ${response.errorMessage}`,
|
|
).not.toBe("error");
|
|
if (response.errorMessage) {
|
|
expect(response.errorMessage).not.toContain("call_id");
|
|
expect(response.errorMessage).not.toContain("too long");
|
|
}
|
|
},
|
|
30000,
|
|
);
|
|
|
|
it.skipIf(!codexToken)(
|
|
"openai-codex should handle prefilled context with long pipe-separated IDs",
|
|
async () => {
|
|
const model = getModel("openai-codex", "gpt-5.2-codex");
|
|
const messages = buildPrefilledMessages();
|
|
|
|
const response = await completeSimple(
|
|
model,
|
|
{
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages,
|
|
tools: [echoTool],
|
|
},
|
|
{ apiKey: codexToken },
|
|
);
|
|
|
|
// Should NOT fail with ID validation error
|
|
expect(
|
|
response.stopReason,
|
|
`Codex error: ${response.errorMessage}`,
|
|
).not.toBe("error");
|
|
if (response.errorMessage) {
|
|
expect(response.errorMessage).not.toContain("id");
|
|
expect(response.errorMessage).not.toContain("additional characters");
|
|
}
|
|
},
|
|
30000,
|
|
);
|
|
});
|