fix(ai): avoid truncated tool calls (#46040)

This commit is contained in:
Aiden Cline 2026-08-28 18:11:40 -05:00 committed by GitHub
parent 8e7190f795
commit cf014bf2c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 4 deletions

View file

@ -1054,10 +1054,10 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
events.push(...result.events)
}
const contentFiltered = finishReason?.normalized === "content-filter"
const incompleteTools = finishReason?.normalized === "content-filter" || finishReason?.normalized === "length"
if (
finishReason !== undefined &&
!contentFiltered &&
!incompleteTools &&
state.finishReason === undefined &&
Object.keys(pendingTools).length
)
@ -1067,10 +1067,10 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
ProviderShared.encodeJson(event),
)
// A content filter terminates the response without confirming pending tool calls.
// Filtering or truncation terminates the response without confirming pending tool calls.
const finished =
finishReason !== undefined &&
!contentFiltered &&
!incompleteTools &&
state.finishReason === undefined &&
Object.keys(tools).length > 0
? yield* ToolStream.finishAll(ADAPTER, tools)

View file

@ -1516,6 +1516,62 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("does not finalize streamed tool calls when output is truncated", () =>
Effect.gen(function* () {
const body = sseEvents(
deltaChunk({
tool_calls: [{ index: 0, id: "call_1", function: { name: "lookup", arguments: '{"query":"weather"}' } }],
}),
deltaChunk({}, "length"),
)
const response = yield* LLMClient.generate(
LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(response.events).toEqual([
{ type: "step-start", index: 0 },
{
type: "tool-input-start",
id: "call_1",
name: "lookup",
providerExecuted: undefined,
providerMetadata: undefined,
},
{
type: "tool-input-delta",
id: "call_1",
name: "lookup",
text: '{"query":"weather"}',
input: { query: "weather" },
},
{
type: "step-finish",
index: 0,
reason: { normalized: "length", raw: "length" },
usage: undefined,
providerMetadata: undefined,
},
{ type: "finish", reason: { normalized: "length", raw: "length" }, usage: undefined },
])
expect(response.toolCalls).toEqual([])
const missingIdentity = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
deltaChunk({ tool_calls: [{ index: 0, id: "call_2", function: { arguments: "{}" } }] }),
deltaChunk({}, "length"),
),
),
),
)
expect(missingIdentity.finishReason).toEqual({ normalized: "length", raw: "length" })
expect(missingIdentity.toolCalls).toEqual([])
}),
)
it.effect("ignores empty identity fields on later tool call deltas", () =>
Effect.gen(function* () {
const body = sseEvents(