|
|
@@ -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: "<path>/root</path>",
|
|
|
+ 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: "<path>/root</path>",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ 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) {
|