fix(ai): preserve function arguments with empty custom payloads

Prefer function payloads when malformed OpenAI-compatible tool-call deltas also include an empty custom object, preserving parsed arguments.

Fixes #7160

Co-authored-by: Sunny Young <isunnyyoung@gmail.com>
This commit is contained in:
Armin Ronacher
2026-07-29 23:54:14 +02:00
committed by GitHub
co-authored by Sunny Young
parent 21f579a9a7
commit 34239180ac
2 changed files with 52 additions and 4 deletions
+3 -4
View File
@@ -383,9 +383,8 @@ export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptio
if (!block) {
// Note: the "input" fallback here should/must not be taken. in case the LLM makes up
// a tool we don't knwo about, we at least have a place to stash our stuff.
const customInputProperty = toolCall.custom
? (grammarToolInputProperties.get(name) ?? "input")
: undefined;
const customInputProperty =
toolCall.custom && !toolCall.function ? (grammarToolInputProperties.get(name) ?? "input") : undefined;
const hasCustomInput = customInputProperty !== undefined;
block = {
type: "toolCall",
@@ -421,7 +420,7 @@ export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptio
if (!block.name && name) {
block.name = name;
}
if (toolCall.custom && !block.customInput) {
if (toolCall.custom && !toolCall.function && !block.customInput) {
const customInputProperty = grammarToolInputProperties.get(block.name) ?? "input";
block.arguments = { [customInputProperty]: "" };
block.customInput = {
@@ -653,6 +653,55 @@ describe("openai-completions tool_choice", () => {
expect(response.errorMessage).toBe("Stream ended without finish_reason");
});
it("ignores empty custom objects on function tool call deltas", async () => {
mockState.chunks = [
{
id: "chatcmpl-empty-custom",
choices: [
{
delta: {
tool_calls: [
{
index: 0,
id: "call_1",
type: "function",
function: { name: "read", arguments: '{"path":"README.md"}' },
custom: {},
},
],
},
finish_reason: "tool_calls",
},
],
},
];
const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!;
const model = { ...baseModel, api: "openai-completions" } as const;
const tool: Tool = {
name: "read",
description: "Read a file",
parameters: Type.Object({ path: Type.String() }),
};
const response = await streamSimple(
model,
{
messages: [{ role: "user", content: "Read README.md", timestamp: Date.now() }],
tools: [tool],
},
{ apiKey: "test" },
).result();
expect(response.content).toEqual([
{
type: "toolCall",
id: "call_1",
name: "read",
arguments: { path: "README.md" },
},
]);
});
it("coalesces tool call deltas by stable index when provider mutates ids mid-stream", async () => {
mockState.chunks = [
{