diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index cbf767b4bd..8719e348d7 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -13,7 +13,7 @@ import { ProviderID, ModelID } from "../../src/provider/schema" import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import type { Agent } from "../../src/agent/agent" -import type { MessageV2 } from "../../src/session/message-v2" +import { MessageV2 } from "../../src/session/message-v2" import { SessionID, MessageID } from "../../src/session/schema" import { AppRuntime } from "../../src/effect/app-runtime" @@ -909,6 +909,269 @@ describe("session.llm.stream", () => { }) }) + test("sends anthropic tool_use blocks with tool_result immediately after them", async () => { + const server = state.server + if (!server) { + throw new Error("Server not initialized") + } + + const source = await loadFixture("anthropic", "claude-opus-4-6") + const model = source.model + const chunks = [ + { + type: "message_start", + message: { + id: "msg-tool-order", + model: model.id, + usage: { + input_tokens: 3, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + }, + }, + }, + { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }, + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: "ok" }, + }, + { type: "content_block_stop", index: 0 }, + { + type: "message_delta", + delta: { stop_reason: "end_turn", stop_sequence: null, container: null }, + usage: { + input_tokens: 3, + output_tokens: 2, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + }, + }, + { type: "message_stop" }, + ] + const request = waitRequest("/messages", createEventResponse(chunks)) + + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + enabled_providers: ["anthropic"], + provider: { + anthropic: { + name: "Anthropic", + env: ["ANTHROPIC_API_KEY"], + npm: "@ai-sdk/anthropic", + api: "https://api.anthropic.com/v1", + models: { + [model.id]: model, + }, + options: { + apiKey: "test-anthropic-key", + baseURL: `${server.url.origin}/v1`, + }, + }, + }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const resolved = await getModel(ProviderID.make("anthropic"), ModelID.make(model.id)) + const sessionID = SessionID.make("session-test-anthropic-tools") + const agent = { + name: "test", + mode: "primary", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + } satisfies Agent.Info + const user = { + id: MessageID.make("user-anthropic-tools"), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { providerID: ProviderID.make("anthropic"), modelID: resolved.id, variant: "max" }, + } satisfies MessageV2.User + + const input = [ + { + info: { + id: "msg_user", + sessionID, + role: "user", + time: { created: 1 }, + agent: "gentleman", + model: { providerID: "anthropic", modelID: "claude-opus-4-6", variant: "max" }, + }, + parts: [ + { + id: "p_user", + sessionID, + messageID: "msg_user", + type: "text", + text: "Can you check whether there are any PDF files in my home directory?", + }, + ], + }, + { + info: { + id: "msg_call", + sessionID, + parentID: "msg_user", + role: "assistant", + mode: "gentleman", + agent: "gentleman", + variant: "max", + path: { cwd: "/root", root: "/" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: "claude-opus-4-6", + providerID: "anthropic", + time: { created: 2, completed: 3 }, + finish: "tool-calls", + }, + parts: [ + { + id: "p_step", + sessionID, + messageID: "msg_call", + type: "step-start", + }, + { + id: "p_read", + sessionID, + messageID: "msg_call", + type: "tool", + tool: "read", + callID: "toolu_01N8mDEzG8DSTs7UPHFtmgCT", + state: { + status: "completed", + input: { filePath: "/root" }, + output: "/root", + metadata: {}, + title: "root", + time: { start: 10, end: 11 }, + }, + }, + { + id: "p_glob", + sessionID, + messageID: "msg_call", + type: "tool", + tool: "glob", + callID: "toolu_01APxrADs7VozN8uWzw9WwHr", + state: { + status: "completed", + input: { pattern: "**/*.pdf", path: "/root" }, + output: "No files found", + metadata: {}, + title: "root", + time: { start: 12, end: 13 }, + }, + }, + { + id: "p_text", + sessionID, + messageID: "msg_call", + type: "text", + text: "I checked your home directory and looked for PDF files.", + time: { start: 14, end: 15 }, + }, + ], + }, + ] as any[] + + await drain({ + user, + sessionID, + model: resolved, + agent, + system: [], + messages: await MessageV2.toModelMessages(input as any, resolved), + tools: { + read: tool({ + description: "Stub read tool", + inputSchema: z.object({ + filePath: z.string(), + }), + execute: async () => ({ output: "stub" }), + }), + glob: tool({ + description: "Stub glob tool", + inputSchema: z.object({ + pattern: z.string(), + path: z.string().optional(), + }), + execute: async () => ({ output: "stub" }), + }), + }, + }) + + const capture = await request + const body = capture.body + + expect(capture.url.pathname.endsWith("/messages")).toBe(true) + expect(body.messages).toStrictEqual([ + { + role: "user", + content: [{ type: "text", text: "Can you check whether there are any PDF files in my home directory?" }], + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "I checked your home directory and looked for PDF files.", + cache_control: { + type: "ephemeral", + }, + }, + { + type: "tool_use", + id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT", + name: "read", + input: { filePath: "/root" }, + }, + { + type: "tool_use", + id: "toolu_01APxrADs7VozN8uWzw9WwHr", + name: "glob", + input: { pattern: "**/*.pdf", path: "/root" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu_01N8mDEzG8DSTs7UPHFtmgCT", + content: "/root", + }, + { + type: "tool_result", + tool_use_id: "toolu_01APxrADs7VozN8uWzw9WwHr", + content: "No files found", + cache_control: { + type: "ephemeral", + }, + }, + ], + }, + ]) + }, + }) + }) + test("sends Google API payload for Gemini models", async () => { const server = state.server if (!server) {