summaryrefslogtreecommitdiff
path: root/libpanto/src/provider_openai_chat.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/provider_openai_chat.zig')
-rw-r--r--libpanto/src/provider_openai_chat.zig84
1 files changed, 78 insertions, 6 deletions
diff --git a/libpanto/src/provider_openai_chat.zig b/libpanto/src/provider_openai_chat.zig
index a05c7ba..4ac1e11 100644
--- a/libpanto/src/provider_openai_chat.zig
+++ b/libpanto/src/provider_openai_chat.zig
@@ -466,12 +466,12 @@ const StreamState = struct {
}
const tu = &self.active_tool.?;
- // Append identity fragments. Most providers send id+name whole on
- // the first delta and never repeat them, but appending is the only
- // correct behavior across the full range of OpenAI-compatible
- // backends — some chunk these strings.
- if (d.id) |s| try tu.id_buf.appendSlice(self.allocator, s);
- if (d.name) |s| try tu.name_buf.appendSlice(self.allocator, s);
+ // Some OpenAI-compatible backends send identity as fragments
+ // (`call_` then `xyz`), while others resend the cumulative/full value
+ // on later argument chunks. Accept both without turning repeats into
+ // runaway ids like `call_xyzcall_xyz...`.
+ if (d.id) |s| try mergeStreamingField(self.allocator, &tu.id_buf, s);
+ if (d.name) |s| try mergeStreamingField(self.allocator, &tu.name_buf, s);
// Defer `onBlockStart` until args begin. The first argument
// fragment is our signal that identity is likely settled enough
@@ -601,6 +601,24 @@ const StreamState = struct {
try out.push(.{ .block_start = .{ .block_type = .ToolUse, .index = tu.block_index } });
}
+ fn mergeStreamingField(
+ allocator: Allocator,
+ buf: *conversation.TextualBlock,
+ piece: []const u8,
+ ) !void {
+ if (piece.len == 0) return;
+ if (buf.items.len == 0) {
+ try buf.appendSlice(allocator, piece);
+ } else if (std.mem.eql(u8, buf.items, piece)) {
+ return;
+ } else if (std.mem.startsWith(u8, piece, buf.items)) {
+ buf.clearRetainingCapacity();
+ try buf.appendSlice(allocator, piece);
+ } else {
+ try buf.appendSlice(allocator, piece);
+ }
+ }
+
/// End the stream: close any open text/thinking block, close the still-
/// active tool_use (if any), then commit the assembled assistant
/// Message to the conversation and push the terminal `message_complete`.
@@ -988,6 +1006,60 @@ test "fragmented tool_call id and name are reassembled" {
try testing.expectEqualStrings("{\"host\":\"a.com\"}", tu.input.items);
}
+test "repeated full tool_call id and name do not accumulate" {
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+ try addUserText(&conv, "read it");
+
+ const events = [_][]const u8{
+ \\{"choices":[{"delta":{"role":"assistant"}}]}
+ ,
+ \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"chatcmpl-tool-abc","type":"function","function":{"name":"std__read","arguments":"{\"path\""}}]}}]}
+ ,
+ \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"chatcmpl-tool-abc","function":{"name":"std__read","arguments":":\"a\"}"}}]}}]}
+ ,
+ \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]}
+ ,
+ "[DONE]",
+ };
+
+ try runStreamedTurn(allocator, &conv, null, &events);
+
+ const tu = conv.messages.items[1].content.items[0].ToolUse;
+ try testing.expectEqualStrings("chatcmpl-tool-abc", tu.id);
+ try testing.expectEqualStrings("std.read", tu.name);
+ try testing.expectEqualStrings("{\"path\":\"a\"}", tu.input.items);
+}
+
+test "cumulative tool_call id and name replace their prefix" {
+ const allocator = testing.allocator;
+
+ var conv = conversation.Conversation.init(allocator);
+ defer conv.deinit();
+ try addUserText(&conv, "read it");
+
+ const events = [_][]const u8{
+ \\{"choices":[{"delta":{"role":"assistant"}}]}
+ ,
+ \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"chatcmpl-tool-","type":"function","function":{"name":"std_","arguments":"{\"path\""}}]}}]}
+ ,
+ \\{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"chatcmpl-tool-abc","function":{"name":"std__read","arguments":":\"a\"}"}}]}}]}
+ ,
+ \\{"choices":[{"delta":{},"finish_reason":"tool_calls"}]}
+ ,
+ "[DONE]",
+ };
+
+ try runStreamedTurn(allocator, &conv, null, &events);
+
+ const tu = conv.messages.items[1].content.items[0].ToolUse;
+ try testing.expectEqualStrings("chatcmpl-tool-abc", tu.id);
+ try testing.expectEqualStrings("std.read", tu.name);
+ try testing.expectEqualStrings("{\"path\":\"a\"}", tu.input.items);
+}
+
test "inbound wire tool name is decoded to dotted form (even split across __)" {
// The model echoes the wire name it was given (`std__read`). It is
// decoded to the internal `std.read` for the conversation/session/