forked from Mirror/pi
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:
co-authored by
Sunny Young
parent
21f579a9a7
commit
34239180ac
@@ -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 = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user