import { describe, expect, it } from "bun:test";
import { convertAnthropicMessages } from "@oh-my-pi/pi-ai/providers/anthropic";
import type { AssistantMessage, Model, ToolResultMessage, UserMessage } from "@oh-my-pi/pi-ai/types";
* Issue #814: Z.AI returns 500
* "'ClaudeContentBlockToolResult' object has no attribute 'id'" on every
* request that contains a tool_result block.
*
* Z.AI's Python proxy at api.z.ai/api/anthropic deserializes tool_result
* blocks into a class that accesses `.id`, even though Anthropic's API only
* carries `tool_use_id`. As a workaround, OMP must include `id` (aliased to
* `tool_use_id`) on tool_result blocks targeted at z.ai. Standard Anthropic
* endpoints must remain unchanged (no `id` field).
*/
const baseModel: Omit<Model<"anthropic-messages">, "provider" | "baseUrl"> = {
api: "anthropic-messages",
id: "glm-4.6",
name: "GLM-4.6",
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
maxTokens: 8192,
contextWindow: 200000,
reasoning: false,
};
const zaiModel: Model<"anthropic-messages"> = {
...baseModel,
provider: "zai",
baseUrl: "https://api.z.ai/api/anthropic",
};
const anthropicModel: Model<"anthropic-messages"> = {
...baseModel,
id: "claude-3-5-sonnet-20241022",
name: "Claude 3.5 Sonnet",
provider: "anthropic",
baseUrl: "https://api.anthropic.com",
};
const user: UserMessage = {
role: "user",
content: "run the tool",
timestamp: Date.now(),
};
const assistant: AssistantMessage = {
role: "assistant",
content: [
{
type: "toolCall",
id: "toolu_abc123",
name: "bash",
arguments: { command: "ls" },
},
],
api: "anthropic-messages",
provider: "anthropic",
model: "glm-4.6",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: Date.now(),
};
const toolResult: ToolResultMessage = {
role: "toolResult",
toolCallId: "toolu_abc123",
toolName: "bash",
content: [{ type: "text", text: "file1\nfile2" }],
isError: false,
timestamp: Date.now(),
};
function getToolResultBlock(model: Model<"anthropic-messages">): Record<string, unknown> {
const params = convertAnthropicMessages([user, assistant, toolResult], model, false);
const last = params.at(-1);
expect(last?.role).toBe("user");
const blocks = last?.content as unknown as Array<Record<string, unknown>>;
expect(Array.isArray(blocks)).toBe(true);
const block = blocks.find(b => b.type === "tool_result");
expect(block).toBeDefined();
return block as Record<string, unknown>;
}
describe("issue #814: z.ai tool_result id workaround", () => {
it("includes `id` aliased to `tool_use_id` on tool_result blocks for z.ai", () => {
const block = getToolResultBlock(zaiModel);
expect(block.tool_use_id).toBe("toolu_abc123");
expect(block.id).toBe("toolu_abc123");
});
it("does not include `id` on tool_result blocks for api.anthropic.com", () => {
const block = getToolResultBlock(anthropicModel);
expect(block.tool_use_id).toBe("toolu_abc123");
expect("id" in block).toBe(false);
});
});