diff options
Diffstat (limited to 'libpanto/src/provider_openai_chat.zig')
| -rw-r--r-- | libpanto/src/provider_openai_chat.zig | 39 |
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); |
