mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 17:52:31 +03:00
fix: buffer and normalize Responses tool-call argument deltas, stripping optional null before reaching the client (#9168)
Refs: base-red #9737
This commit is contained in:
committed by
GitHub
parent
e7d9055314
commit
492f9ddc4a
@@ -0,0 +1 @@
|
||||
- fix(translator): buffer and normalize upstream tool-call argument deltas so optional null values are stripped before reaching the client (#9168)
|
||||
@@ -874,6 +874,7 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
|
||||
if (state.currentToolCallId) state.toolCallIdsSeen.add(state.currentToolCallId);
|
||||
|
||||
const toolName = normalizeToolName(item.name);
|
||||
state.currentToolName = toolName; // track for schema lookup at done time
|
||||
if (!toolName) {
|
||||
// Some Responses providers briefly emit placeholder/empty tool names.
|
||||
// Defer emission until output_item.done in case the final name is populated there.
|
||||
@@ -919,26 +920,9 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
|
||||
state.currentToolCallArgsBuffer = (state.currentToolCallArgsBuffer || "") + argsDelta;
|
||||
if (state.currentToolCallDeferred) return null;
|
||||
|
||||
return {
|
||||
id: state.chatId,
|
||||
object: "chat.completion.chunk",
|
||||
created: state.created,
|
||||
model: state.model || "gpt-4",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: state.toolCallIndex,
|
||||
function: { arguments: argsDelta },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
// #9168: buffer arguments until output_item.done for schema-aware null normalization
|
||||
// Previously emitted raw null values for optional enum fields (e.g. isolation: null).
|
||||
return null;
|
||||
}
|
||||
|
||||
// Function call done — emit args chunk from item.arguments when no deltas were received,
|
||||
@@ -1011,6 +995,35 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) {
|
||||
if (item.arguments != null && !buffered) {
|
||||
const argsToEmit = stripEmptyOptionalToolArgs(item.arguments, toolName, toolSchema);
|
||||
|
||||
const argsStr = typeof argsToEmit === "string" ? argsToEmit : JSON.stringify(argsToEmit);
|
||||
if (argsStr) {
|
||||
return {
|
||||
id: state.chatId,
|
||||
object: "chat.completion.chunk",
|
||||
created: state.created,
|
||||
model: state.model || "gpt-4",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: currentIndex,
|
||||
function: { arguments: argsStr },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
} else if (buffered) {
|
||||
// #9168: deltas were buffered — normalize against the original client schema
|
||||
// and emit the cleaned arguments once, stripping optional null values that
|
||||
// would otherwise reach the client raw.
|
||||
const argsToEmit = stripEmptyOptionalToolArgs(buffered, toolName, toolSchema);
|
||||
|
||||
const argsStr = typeof argsToEmit === "string" ? argsToEmit : JSON.stringify(argsToEmit);
|
||||
if (argsStr) {
|
||||
return {
|
||||
|
||||
@@ -40,13 +40,21 @@ test("Responses->Chat: first tool_call chunk announces role=assistant", () => {
|
||||
);
|
||||
assert.equal(first.choices[0].delta.tool_calls[0].function.name, "get_weather");
|
||||
|
||||
// Subsequent argument deltas must NOT repeat the role announcement.
|
||||
// #9168: arguments deltas are buffered until output_item.done for schema normalization.
|
||||
const next = openaiResponsesToOpenAIResponse(
|
||||
{ type: "response.function_call_arguments.delta", delta: '{"x":1}' },
|
||||
state
|
||||
);
|
||||
assert.ok(next, "should emit a chunk for arguments.delta");
|
||||
assert.equal(next.choices[0].delta.role, undefined, "only the first delta announces the role");
|
||||
assert.equal(next, null, "arguments delta should buffer until output_item.done");
|
||||
|
||||
// The args are emitted at output_item.done, and the role is not re-announced.
|
||||
const done = openaiResponsesToOpenAIResponse(
|
||||
{ type: "response.output_item.done", item: { type: "function_call", call_id: "call_abc", name: "get_weather" } },
|
||||
state
|
||||
);
|
||||
assert.ok(done, "should emit a chunk for output_item.done");
|
||||
assert.equal(done.choices[0].delta.role, undefined, "role announcement already happened on first chunk");
|
||||
assert.equal(done.choices[0].delta.tool_calls[0].function.arguments, '{"x":1}');
|
||||
});
|
||||
|
||||
test("Responses->Chat: first text chunk announces role=assistant", () => {
|
||||
|
||||
@@ -203,7 +203,8 @@ test("Responses -> OpenAI: incremental tool call events + response.completed sna
|
||||
},
|
||||
state
|
||||
);
|
||||
assert.ok(args, "should emit args delta chunk");
|
||||
// #9168: arguments deltas are buffered until output_item.done for schema normalization
|
||||
assert.equal(args, null, "args delta should buffer until output_item.done");
|
||||
|
||||
openaiResponsesToOpenAIResponse(
|
||||
{
|
||||
|
||||
@@ -474,7 +474,7 @@ test("Responses -> OpenAI: tool-call delta, reasoning delta and completed usage
|
||||
},
|
||||
state
|
||||
);
|
||||
openaiResponsesToOpenAIResponse(
|
||||
const done = openaiResponsesToOpenAIResponse(
|
||||
{
|
||||
type: "response.output_item.done",
|
||||
item: { type: "function_call", call_id: "call_2", name: "weather" },
|
||||
@@ -497,7 +497,10 @@ test("Responses -> OpenAI: tool-call delta, reasoning delta and completed usage
|
||||
);
|
||||
|
||||
assert.equal(added.choices[0].delta.tool_calls[0].function.name, "weather");
|
||||
assert.equal(args.choices[0].delta.tool_calls[0].function.arguments, '{"city":"SP"}');
|
||||
// #9168: function_call_arguments.delta is buffered and returns null;
|
||||
// arguments are emitted by output_item.done instead.
|
||||
assert.equal(args, null);
|
||||
assert.equal(done.choices[0].delta.tool_calls[0].function.arguments, '{"city":"SP"}');
|
||||
assert.equal(reasoning.choices[0].delta.reasoning_content, "Need weather info.");
|
||||
assert.equal(completed.choices[0].finish_reason, "tool_calls");
|
||||
const comp = completed as {
|
||||
|
||||
Reference in New Issue
Block a user