summaryrefslogtreecommitdiff
path: root/libpanto/src/provider_openai_chat.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-12 14:49:51 -0600
committert <t@tjp.lol>2026-06-12 16:04:37 -0600
commitef4647f1d324cff43bb444ef4df6c253faed0d52 (patch)
tree9c5b131df15d86ed4b5473e3d1754466d0bbac63 /libpanto/src/provider_openai_chat.zig
parentecadcfa7a1e549d6ddb8611bdf44cac852064cea (diff)
improve errors: retries, surfacing provider errors to the user
Diffstat (limited to 'libpanto/src/provider_openai_chat.zig')
-rw-r--r--libpanto/src/provider_openai_chat.zig31
1 files changed, 31 insertions, 0 deletions
diff --git a/libpanto/src/provider_openai_chat.zig b/libpanto/src/provider_openai_chat.zig
index 5152d3e..5713b5d 100644
--- a/libpanto/src/provider_openai_chat.zig
+++ b/libpanto/src/provider_openai_chat.zig
@@ -214,8 +214,14 @@ pub const ResumableResponse = struct {
const vtable: provider_mod.ProviderStream.VTable = .{
.produce = produceVT,
.deinit = deinitVT,
+ .last_error = lastErrorVT,
};
+ fn lastErrorVT(ptr: *anyopaque) ?[]const u8 {
+ const self: *ResumableResponse = @ptrCast(@alignCast(ptr));
+ return self.state.stream_error_message;
+ }
+
fn produceVT(ptr: *anyopaque, out: *EventQueue) anyerror!ProduceStatus {
const self: *ResumableResponse = @ptrCast(@alignCast(ptr));
return self.produce(out);
@@ -339,6 +345,11 @@ const StreamState = struct {
/// `stream_options.include_usage: true` AND the server honored it).
usage: ?provider_mod.Usage = null,
+ /// Owned, human-readable description of a mid-stream error embedded in an
+ /// HTTP-200 SSE body, surfaced to the agent via `ProviderStream.lastError`
+ /// so the retry notice can explain *why*.
+ stream_error_message: ?[]u8 = null,
+
const ToolUseInProgress = struct {
/// Block index emitted to the receiver for this tool call's
/// onBlockStart / onContentDelta / onBlockComplete callbacks.
@@ -385,6 +396,23 @@ const StreamState = struct {
self.blocks.deinit(self.allocator);
if (self.active_tool) |*tu| tu.deinit(self.allocator);
self.closed_tool_indices.deinit();
+ if (self.stream_error_message) |s| self.allocator.free(s);
+ }
+
+ /// Record a readable description of an embedded stream error, combining
+ /// the error `type` and `message` into one owned string (either may be
+ /// absent). Replaces any previous value.
+ fn setStreamErrorMessage(self: *StreamState, kind: ?[]const u8, message: ?[]const u8) !void {
+ if (self.stream_error_message) |old| self.allocator.free(old);
+ self.stream_error_message = null;
+ self.stream_error_message = if (kind != null and message != null)
+ try std.fmt.allocPrint(self.allocator, "{s}: {s}", .{ kind.?, message.? })
+ else if (kind) |k|
+ try self.allocator.dupe(u8, k)
+ else if (message) |m|
+ try self.allocator.dupe(u8, m)
+ else
+ null;
}
/// Close the active text/thinking block (if any) and emit
@@ -683,6 +711,9 @@ fn handleEvent(
d.error_type, d.error_message,
});
}
+ // Stash a readable description so the agent's retry notice can
+ // explain *why* the stream failed. Owned by `state`.
+ state.setStreamErrorMessage(d.error_type, d.error_message) catch {};
return error.ProviderStreamMalformed;
}