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.zig39
1 files changed, 25 insertions, 14 deletions
diff --git a/libpanto/src/provider_openai_chat.zig b/libpanto/src/provider_openai_chat.zig
index c454679..ca01d7c 100644
--- a/libpanto/src/provider_openai_chat.zig
+++ b/libpanto/src/provider_openai_chat.zig
@@ -46,6 +46,11 @@ pub const OpenAIChatRequest = struct {
io: Io,
config: *const config_mod.OpenAIChatConfig,
http_client: *http.Client,
+ /// Optional diagnostic side-channel. When non-null, classified failures
+ /// stash the HTTP status code and any `Retry-After` here for the agent's
+ /// retry policy. Strings written here are not owned by the diagnostic;
+ /// they live only as long as this request object.
+ diag: ?*provider_mod.ProviderDiagnostic = null,
pub fn streamStep(
self: *OpenAIChatRequest,
@@ -133,19 +138,17 @@ pub const OpenAIChatRequest = struct {
try err_buf.appendSlice(self.allocator, tmp[0..n]);
if (err_buf.items.len > 16 * 1024) break;
}
- std.log.err("openai_chat HTTP {d}: {s}", .{
- @intFromEnum(response.head.status),
- err_buf.items,
- });
- // Detect a context-overflow rejection so the caller can
- // compact and retry rather than treating it as a hard error.
- // OpenAI-compatible APIs return HTTP 400 with a
- // `context_length_exceeded` code / "maximum context length"
- // message on the input side.
- if (@intFromEnum(response.head.status) == 400 and provider_mod.isContextOverflowBody(err_buf.items)) {
- return error.ContextOverflow;
+ const status: u16 = @intFromEnum(response.head.status);
+ std.log.err("openai_chat HTTP {d}: {s}", .{ status, err_buf.items });
+ // Classify the status into a retryable/terminal provider error.
+ // HTTP 400 with a context marker becomes `ContextOverflow` so the
+ // caller can compact and retry rather than hard-fail.
+ const classified = provider_mod.classifyHttpStatus(status, err_buf.items);
+ if (self.diag) |d| {
+ d.status_code = status;
+ d.retry_after_ms = provider_mod.retryAfterFromHead(response.head);
}
- return error.HttpError;
+ return classified;
}
// Stream the body through the SSE parser and event handler.
@@ -172,7 +175,10 @@ pub const OpenAIChatRequest = struct {
while (true) {
const n = body_reader.readVec(&vecs) catch |err| switch (err) {
error.EndOfStream => break,
- else => return err,
+ // A transport read failure mid-stream (reset, TLS, timeout)
+ // before `[DONE]` means no assistant message was committed.
+ // Surface it as a retryable malformed-stream error.
+ else => return error.ProviderStreamMalformed,
};
if (n == 0) continue;
@@ -576,7 +582,7 @@ fn handleEvent(
d.error_type, d.error_message,
});
}
- return error.StreamError;
+ return error.ProviderStreamMalformed;
}
if (!state.started and d.role != null) {
@@ -636,6 +642,7 @@ const NoopReceiver = struct {
.onBlockComplete = noopBlockComplete,
.onMessageComplete = noopMsgComplete,
.onError = noopErr,
+ .onProviderRetry = noopRetry,
};
fn noopMsgStart(_: *anyopaque, _: conversation.MessageRole) anyerror!void {}
fn noopBlockStart(_: *anyopaque, _: provider_mod.ContentBlockType, _: usize) anyerror!void {}
@@ -644,6 +651,7 @@ const NoopReceiver = struct {
fn noopBlockComplete(_: *anyopaque, _: usize, _: conversation.ContentBlock) anyerror!void {}
fn noopMsgComplete(_: *anyopaque, _: conversation.Message, _: ?provider_mod.Usage) anyerror!void {}
fn noopErr(_: *anyopaque, _: anyerror) void {}
+ fn noopRetry(_: *anyopaque, _: provider_mod.ProviderRetryInfo) void {}
};
/// Feed a sequence of SSE event payloads through the state machine as if
@@ -883,8 +891,11 @@ const RecordingReceiver = struct {
.onBlockComplete = onBlockComplete,
.onMessageComplete = onMessageComplete,
.onError = onError,
+ .onProviderRetry = onProviderRetry,
};
+ fn onProviderRetry(_: *anyopaque, _: provider_mod.ProviderRetryInfo) void {}
+
fn record(self: *RecordingReceiver, s: []const u8) !void {
const owned = try self.allocator.dupe(u8, s);
try self.events.append(self.allocator, owned);