test(core): cover Gemini partial recovery

This commit is contained in:
kitlangton
2026-08-22 01:24:38 +00:00
committed by Hona
parent 1b0435e464
commit aa2e22b62d
3 changed files with 205 additions and 0 deletions
@@ -0,0 +1,34 @@
{
"version": 1,
"metadata": {
"tags": [
"prefix:gemini-recovery",
"provider:google",
"protocol:gemini",
"tool",
"recovery"
],
"name": "gemini-recovery/accepts-an-unsigned-settled-tool-call-followed-by-continuation",
"recordedAt": "2026-08-22T01:23:18.801Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:streamGenerateContent?alt=sse",
"headers": {
"content-type": "application/json"
},
"body": "{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"Look up the weather in Paris.\"}]},{\"role\":\"model\",\"parts\":[{\"text\":\"I should use the weather tool.\"},{\"functionCall\":{\"name\":\"lookup_weather\",\"args\":{\"city\":\"Paris\"}},\"thoughtSignature\":\"skip_thought_signature_validator\"}]},{\"role\":\"user\",\"parts\":[{\"functionResponse\":{\"name\":\"lookup_weather\",\"response\":{\"name\":\"lookup_weather\",\"content\":\"sunny\"}}}]},{\"role\":\"user\",\"parts\":[{\"text\":\"The previous response was interrupted. Continue from where you left off without repeating completed content.\"}]}],\"tools\":[{\"functionDeclarations\":[{\"name\":\"lookup_weather\",\"description\":\"Return benign weather data for a city.\",\"parameters\":{\"required\":[\"city\"],\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}}}}]}],\"toolConfig\":{\"functionCallingConfig\":{\"mode\":\"NONE\"}},\"generationConfig\":{\"maxOutputTokens\":256,\"temperature\":0}}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"The weather in Paris is currently sunny\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 116,\"candidatesTokenCount\": 7,\"totalTokenCount\": 368,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 116}],\"thoughtsTokenCount\": 245,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"BfqIaun_Gbau39IP49TwKA\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"\",\"thoughtSignature\": \"EuEKCt4KARFNMg+TrcXumi0KeXb7iZG/dBk9QjybBSM9V7hTN2a0lEXMuWh38tNhXrQCwNFDZ2gKEpa2c1lj/+wwzJAvjceCFoRenOGfBEUGRavc6oQXLKiWZPGaHV0G4gIo2uosx8SNZc4E5Dko2IYPymcUzDgsqZK2eToDoepaM8cekix+6c/z2MZiRnJ0QbS3f/7r+UYLOIi7eWbW0G6v+gwPYae4kRuifL6iKEV5eG+R4AGlGfOI0dgamDfEFfGiwg+rkvjKw7DDmYvQVreUcjvFt2OOhKfFWmnvOQrIKkSYpSyvdlylRhKZMU5gwUsKii8xwI5eJVoUoYDoOjr1x8DR3ePkCj/n3b0OMUWunPkdpgE/bUcFMQ+Gzqs7F3sBYDgUiT3F9r5joKZp6dm0I64/gh15J0NCamlr+CRLP+a8tJbEcWZbgELhbFE+2ikJMtoApojvlUbIUsn7sXHQ6042A/MjWgV97DWcG6V1dtLOOrS2Ez2F58TWDW67dTD3o+Ixz82+xDfG4VW0pOiOPOo9n2YCGk9xlSwPG9qDwU6bpi0AFrJVSz2gc8OFXk5uwCooPAN0EmnfljWS5+spS7AmKA2k3S7IZKe0aAw90+Jp4JxklOsfL4XIlQJLLXKmtcnTEdZLYp/JxSL9YRNnoHZJaeBpIWEObE+aX+CuI3OqZZPpldalNeCPM1nCYu26rfBzAaP85zzyi4SFzlXc++E9OPei8BgPoD8XuTb1yK1nTtPqjR8bYvaDm9ef8NaWrYdM+ivVQ/Q/9YAQ8AXuC71dQZExcTQAC5oOnrRP1byo2lHMuytnVk8QrnZBhiJc/TneDFzCYK9nFkhc6s0aaQrBip6YEKPtGmuMmUxJ1dQYFQlgtjEvANB2/jnPtE6u1p039reevLAHMxtijgCqNE0hrVaciI1ZTLTYtO1UquoSXNoHqqZzYuEnLMUKxTbWzIOcVBC4XwP8rkYLwCp9e1oMMBV7FIT6Kv2El7tWrpBt2w+2GgQMt2bAr2PJQf1oj5O/vOXEbcTVumOv29rUJQ65pFpr8K6Q8W6lIbIxOGWLKb/akVl9FJq5lPVp+m3eEYurQcQJ1mB3OEuPrpK4COdiVElarAnaxaMpioa8yCh22kA4eqS+bChUDIZIfCUNRDGI0eO8YMkRo626VLp9IJnexYNBBwBKk5oL7TmrlzRz/YyUEApVNMUjHiRd24i2jq4pQyOdkbYMVi3YFZNQbRuo0EN9dM8vAG4VrAGPj892T2ne5B69Qmhgs8Ow4QrubWd+d6ohdsSyOucVruw1Mr+x79BqiQs+SQ5M/GJCPBYO3QIV3CMGnHNrtvg5YdYE5blCs0MCf+u5mHPPk2TlBuXMJtLUN/YmGhE4u1/ZT9WTIueekSSubPv1eqPwLRHLiwVfKdA/FNqu5YDifgzKMjyIQpXiEIW3OnI4A1ps8K4pMynZ6XXyZuvq74eHnX1vQDdDA27MiWJPrDWqzg5SQnOl026Mcpj0pIex1lN+7av8xAc9PVszdn9qq8WwO7yUOPM10HnE2hY4pDGa3LXo/2VMAbknQOwhEA6vEgGwFVXN2EXYS9fohRgrXbo7hCroJlekcuROXX7sq2JEK751xXWZlaIpujUpNt8uUGRjSIIFKhuYYvrmU7FEN/PgB0eyQ6KQp1iQm7ywzqMF0Ns2XbM6iAlMibLu48G6VplgUWpsNMtbJLLs3QuFeligNdARJ8aMfJarHlRFV/oQXWggnSyTO5wKCOif8IJUWuHR43JStLvFZsOzVvCDduPSdWKs/9vkri3iVbRFCmeTyuJB+xQuz3rbU+iVI9VOe2BIAhKY\"}],\"role\": \"model\"},\"finishReason\": \"MAX_TOKENS\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 116,\"candidatesTokenCount\": 7,\"totalTokenCount\": 368,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 116}],\"thoughtsTokenCount\": 245,\"serviceTier\": \"standard\"},\"modelVersion\": \"gemini-3.5-flash\",\"responseId\": \"BfqIaun_Gbau39IP49TwKA\"}\r\n\r\n"
}
}
]
}
@@ -0,0 +1,57 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, Message, ToolCallPart, ToolDefinition } from "../../src/index.js"
import { configure } from "../../src/providers/google.js"
import { LLMClient } from "../../src/route.js"
import { recordedTests } from "../recorded-test.js"
const model = configure({
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? "fixture",
}).model("gemini-3.5-flash")
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: "gemini-recovery",
provider: "google",
protocol: "gemini",
requires: ["GOOGLE_GENERATIVE_AI_API_KEY"],
})
describe("Gemini interrupted recovery recorded", () => {
recorded.effect.with(
"accepts an unsigned settled tool call followed by continuation",
{ tags: ["tool", "recovery"] },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
id: "recorded_gemini_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: 256, temperature: 0 },
}),
)
expect(response.text.trim().length).toBeGreaterThan(0)
}),
)
})
@@ -0,0 +1,114 @@
import { expect, test } from "bun:test"
import { LLM, Message } from "@opencode-ai/ai"
import { configure } from "@opencode-ai/ai/providers/google"
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 google = Model.Ref.make({
id: Model.ID.make("gemini-3.5-flash"),
providerID: Provider.ID.make("google"),
})
const continuation =
"The previous response was interrupted. Continue from where you left off without repeating completed content."
test("failed Gemini 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: google,
content: [
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "I should use the weather tool.",
state: { thoughtSignature: "signed-reasoning" },
}),
SessionMessage.AssistantTool.make({
type: "tool",
id: "call_weather",
name: "lookup_weather",
executed: false,
providerState: { thoughtSignature: "signed-tool-call" },
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 },
}),
],
google,
)
const prepared = await Effect.runPromise(
compileRequest(
LLM.request({
model: configure({ apiKey: "test" }).model("gemini-3.5-flash"),
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.contents).toEqual([
{ role: "user", parts: [{ text: "Look up the weather." }] },
{
role: "model",
parts: [
{ text: "I should use the weather tool.", thoughtSignature: undefined },
{
functionCall: { id: undefined, name: "lookup_weather", args: { city: "Paris" } },
thoughtSignature: "skip_thought_signature_validator",
},
],
},
{
role: "user",
parts: [
{
functionResponse: {
id: undefined,
name: "lookup_weather",
response: { name: "lookup_weather", content: "sunny" },
},
},
],
},
{ role: "user", parts: [{ text: continuation }] },
])
})