fix(llm): preserve structured tool errors (#33405)

This commit is contained in:
Kit Langton 2026-06-22 19:43:07 +02:00 committed by GitHub
commit 130957288e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 88 additions and 2 deletions

View file

@ -224,6 +224,27 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("preserves structured tool errors for the model", () =>
Effect.gen(function* () {
const error = { error: { type: "unknown", message: "Tool execution interrupted" } }
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "bash", input: {} })]),
Message.tool({ id: "call_1", name: "bash", resultType: "error", result: error }),
],
}),
)
expect(prepared.body.messages.at(-1)).toEqual({
role: "tool",
tool_call_id: "call_1",
content: ProviderShared.encodeJson(error),
})
}),
)
it.effect("continues image tool results as vision input without base64 text", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(

View file

@ -360,6 +360,64 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("preserves structured tool errors for the model", () =>
Effect.gen(function* () {
const error = {
error: { type: "unknown", message: "Tool execution interrupted" },
content: [],
structured: {},
}
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "bash", input: { command: "sleep 10" } })]),
Message.tool({
id: "call_1",
name: "bash",
resultType: "error",
result: error,
}),
],
}),
)
expect(expectToolOutput(prepared.body).output).toBe(ProviderShared.encodeJson(error))
}),
)
it.effect("keeps primitive tool errors as plain text", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "bash", input: {} })]),
Message.tool({ id: "call_1", name: "bash", resultType: "error", result: 503 }),
],
}),
)
expect(expectToolOutput(prepared.body).output).toBe("503")
}),
)
it.effect("keeps non-JSON tool errors as plain text", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "bash", input: {} })]),
Message.tool({ id: "call_1", name: "bash", resultType: "error", result: new Error("boom") }),
],
}),
)
expect(expectToolOutput(prepared.body).output).toBe("Error: boom")
}),
)
// Regression: screenshot/read tool results must stay structured so base64
// image data is not JSON-stringified into `function_call_output.output`.
it.effect("lowers image tool-result content as structured input_image items", () =>