mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-17 06:04:52 +00:00
move pi-mono into companion-cloud as apps/companion-os
- Copy all pi-mono source into apps/companion-os/ - Update Dockerfile to COPY pre-built binary instead of downloading from GitHub Releases - Update deploy-staging.yml to build pi from source (bun compile) before Docker build - Add apps/companion-os/** to path triggers - No more cross-repo dispatch needed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
0250f72976
579 changed files with 206942 additions and 0 deletions
320
packages/ai/test/tool-call-id-normalization.test.ts
Normal file
320
packages/ai/test/tool-call-id-normalization.test.ts
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
/**
|
||||
* 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/pi-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,
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue