mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 14:43:37 -04:00
test(core): cover Anthropic partial recovery
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"prefix:anthropic-recovery",
|
||||
"provider:anthropic",
|
||||
"protocol:anthropic-messages",
|
||||
"tool",
|
||||
"recovery"
|
||||
],
|
||||
"name": "anthropic-recovery/accepts-a-synthetic-settled-tool-call-followed-by-continuation",
|
||||
"recordedAt": "2026-08-22T01:52:23.916Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://opencode.ai/zen/v1/messages",
|
||||
"headers": {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"claude-sonnet-4-6\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Look up the weather in Paris.\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I should use the weather tool.\"},{\"type\":\"tool_use\",\"id\":\"call_weather\",\"name\":\"lookup_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_weather\",\"content\":\"sunny\"}]},{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]},\"cache_control\":{\"type\":\"ephemeral\"}}],\"tool_choice\":{\"type\":\"none\"},\"stream\":true,\"max_tokens\":128,\"temperature\":0}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream; charset=utf-8"
|
||||
},
|
||||
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"msg_011CeGx6WkAQP5ULyRRbRwRw\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":664,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" weather in Paris is currently **sunny**! It looks like a great day to enjoy the city. ☀️\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":664,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":28} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\nevent: ping\ndata: {\"type\":\"ping\",\"cost\":\"0\"}\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { LLM, Message, ToolCallPart, ToolDefinition } from "../../src/index.js"
|
||||
import { configure } from "../../src/providers/anthropic.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
import { recordedTests } from "../recorded-test.js"
|
||||
|
||||
const model = configure({
|
||||
apiKey: process.env.ZEN_TEST_API_KEY ?? "fixture",
|
||||
baseURL: "https://opencode.ai/zen/v1",
|
||||
}).model("claude-sonnet-4-6")
|
||||
const continuation =
|
||||
"The previous response was interrupted. Continue from where you left off without repeating completed content."
|
||||
const lookup = ToolDefinition.make({
|
||||
name: "lookup_weather",
|
||||
description: "Return benign weather data for a city.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { city: { type: "string" } },
|
||||
required: ["city"],
|
||||
},
|
||||
})
|
||||
const recorded = recordedTests({
|
||||
prefix: "anthropic-recovery",
|
||||
provider: "anthropic",
|
||||
protocol: "anthropic-messages",
|
||||
requires: ["ZEN_TEST_API_KEY"],
|
||||
options: { redact: { allowRequestHeaders: ["anthropic-version"] } },
|
||||
})
|
||||
|
||||
describe("Anthropic interrupted recovery recorded", () => {
|
||||
recorded.effect.with(
|
||||
"accepts a synthetic settled tool call followed by continuation",
|
||||
{ tags: ["tool", "recovery"] },
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
id: "recorded_anthropic_interrupted_tool_recovery",
|
||||
model,
|
||||
messages: [
|
||||
Message.user("Look up the weather in Paris."),
|
||||
Message.assistant([
|
||||
{ type: "text", text: "I should use the weather tool." },
|
||||
ToolCallPart.make({ id: "call_weather", name: "lookup_weather", input: { city: "Paris" } }),
|
||||
]),
|
||||
Message.tool({ id: "call_weather", name: "lookup_weather", result: "sunny", resultType: "text" }),
|
||||
Message.user(continuation),
|
||||
],
|
||||
tools: [lookup],
|
||||
toolChoice: "none",
|
||||
generation: { maxTokens: 128, temperature: 0 },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.text.trim().length).toBeGreaterThan(0)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,99 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { LLM } from "@opencode-ai/ai"
|
||||
import { configure } from "@opencode-ai/ai/providers/anthropic"
|
||||
import { compileRequest } from "@opencode-ai/ai/route/client"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Model } from "@opencode-ai/core/model"
|
||||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message"
|
||||
import { DateTime, Effect } from "effect"
|
||||
|
||||
const created = DateTime.makeUnsafe(0)
|
||||
const anthropic = Model.Ref.make({
|
||||
id: Model.ID.make("claude-sonnet-4-6"),
|
||||
providerID: Provider.ID.make("anthropic"),
|
||||
})
|
||||
const continuation =
|
||||
"The previous response was interrupted. Continue from where you left off without repeating completed content."
|
||||
|
||||
test("failed Anthropic tool history replays through the public request boundary", async () => {
|
||||
const messages = toLLMMessages(
|
||||
[
|
||||
SessionMessage.User.make({
|
||||
id: SessionMessage.ID.make("msg_user"),
|
||||
type: "user",
|
||||
text: "Look up the weather.",
|
||||
time: { created },
|
||||
}),
|
||||
SessionMessage.Assistant.make({
|
||||
id: SessionMessage.ID.make("msg_failed"),
|
||||
type: "assistant",
|
||||
agent: Agent.defaultID,
|
||||
model: anthropic,
|
||||
content: [
|
||||
SessionMessage.AssistantReasoning.make({
|
||||
type: "reasoning",
|
||||
text: "I should use the weather tool.",
|
||||
state: { signature: "signed-reasoning" },
|
||||
}),
|
||||
SessionMessage.AssistantTool.make({
|
||||
type: "tool",
|
||||
id: "call_weather",
|
||||
name: "lookup_weather",
|
||||
executed: false,
|
||||
state: SessionMessage.ToolStateCompleted.make({
|
||||
status: "completed",
|
||||
input: { city: "Paris" },
|
||||
content: [{ type: "text", text: "sunny" }],
|
||||
}),
|
||||
time: { created, completed: created },
|
||||
}),
|
||||
],
|
||||
finish: "error",
|
||||
error: { type: "provider.internal", message: "Provider stream failed" },
|
||||
time: { created, completed: created },
|
||||
}),
|
||||
SessionMessage.Synthetic.make({
|
||||
id: SessionMessage.ID.make("msg_continue"),
|
||||
type: "synthetic",
|
||||
text: continuation,
|
||||
time: { created },
|
||||
}),
|
||||
],
|
||||
anthropic,
|
||||
)
|
||||
const prepared = await Effect.runPromise(
|
||||
compileRequest(
|
||||
LLM.request({
|
||||
model: configure({ apiKey: "test" }).model("claude-sonnet-4-6"),
|
||||
messages,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(messages).toMatchObject([
|
||||
{ role: "user" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "I should use the weather tool." },
|
||||
{ type: "tool-call", id: "call_weather", providerMetadata: undefined },
|
||||
],
|
||||
},
|
||||
{ role: "tool", content: [{ type: "tool-result", id: "call_weather" }] },
|
||||
{ role: "user", content: [{ type: "text", text: continuation }] },
|
||||
])
|
||||
expect(prepared.body.messages).toMatchObject([
|
||||
{ role: "user", content: [{ type: "text", text: "Look up the weather." }] },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "I should use the weather tool." },
|
||||
{ type: "tool_use", id: "call_weather", name: "lookup_weather", input: { city: "Paris" } },
|
||||
],
|
||||
},
|
||||
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_weather", content: "sunny" }] },
|
||||
{ role: "user", content: [{ type: "text", text: continuation }] },
|
||||
])
|
||||
})
|
||||
Reference in New Issue
Block a user