diff options
Diffstat (limited to 'libpanto/src/provider_anthropic_messages.zig')
| -rw-r--r-- | libpanto/src/provider_anthropic_messages.zig | 37 |
1 files changed, 33 insertions, 4 deletions
diff --git a/libpanto/src/provider_anthropic_messages.zig b/libpanto/src/provider_anthropic_messages.zig index 8f5d200..95013d1 100644 --- a/libpanto/src/provider_anthropic_messages.zig +++ b/libpanto/src/provider_anthropic_messages.zig @@ -5,7 +5,10 @@ //! //! Responsibilities: //! - Convert `Conversation` → request JSON (delegated to anthropic_messages_json.zig) -//! - POST to `{base_url}/v1/messages` with `stream: true` +//! - POST to `{base_url}/messages` with `stream: true` +//! (caller is responsible for the `/v1` version segment; panto will +//! introduce a new `anthropic_messages_v2` API style for v2 rather than +//! pulling the version into the provider) //! - Read the chunked body, feed bytes through SSEParser //! - Parse each event payload, drive a thin assembly loop, and emit Receiver //! callbacks. Anthropic gives us explicit block boundaries, so no @@ -58,6 +61,12 @@ pub const AnthropicMessagesRequest = struct { .parser = sse_mod.SSEParser.init(self.allocator), .state = .init(self.allocator), }; + rr.state.signature_origin = try conversation.SignatureOrigin.init( + self.allocator, + .anthropic_messages, + self.config.base_url, + self.config.model, + ); errdefer { rr.parser.deinit(); rr.state.deinit(); @@ -65,7 +74,7 @@ pub const AnthropicMessagesRequest = struct { const url = try std.fmt.allocPrint( self.allocator, - "{s}/v1/messages", + "{s}/messages", .{self.config.base_url}, ); defer self.allocator.free(url); @@ -74,6 +83,7 @@ pub const AnthropicMessagesRequest = struct { const body = try json_mod.serializeRequest(self.allocator, self.config, conv, tools); defer self.allocator.free(body); + std.log.debug("anthropic_messages => {s}", .{body}); // Build headers. The four base headers are always present; the // interleaved-thinking beta header is added only when the config @@ -96,7 +106,7 @@ pub const AnthropicMessagesRequest = struct { .value = "interleaved-thinking-2025-05-14", }; } - const base_headers = headers_buf[0 .. if (send_interleaved) @as(usize, 5) else @as(usize, 4)]; + const base_headers = headers_buf[0..if (send_interleaved) @as(usize, 5) else @as(usize, 4)]; // Merge any provider `extra_headers` onto the base set. Freed at the // end of `open` — after the request body has been flushed. const extra_headers = try provider_mod.mergeHeaders( @@ -147,12 +157,20 @@ pub const AnthropicMessagesRequest = struct { if (err_buf.items.len > 16 * 1024) break; } const status: u16 = @intFromEnum(rr.response.head.status); - std.log.err("anthropic_messages HTTP {d}: {s}", .{ status, err_buf.items }); // Anthropic rejects oversized requests with HTTP 400 and a // "prompt is too long" message; `classifyHttpStatus` maps that to // ContextOverflow so the caller can compact and retry. Other // statuses map to retryable/terminal provider errors. const classified = provider_mod.classifyHttpStatus(status, err_buf.items); + // 401/403 is routinely recovered by the turn-runner's forced token + // refresh + reopen; demote it to `.debug` (still in the debug log) + // so a transparent refresh doesn't surface a scary error line. The + // retry layer raises a hard error only if recovery ultimately fails. + if (classified == error.ProviderAuthFailed) { + std.log.debug("anthropic_messages HTTP {d} (recoverable auth): {s}", .{ status, err_buf.items }); + } else { + std.log.err("anthropic_messages HTTP {d}: {s}", .{ status, err_buf.items }); + } if (self.diag) |d| { d.status_code = status; d.retry_after_ms = retry_after_ms; @@ -287,6 +305,7 @@ const StreamState = struct { /// `onMessageComplete`, the latter stamps `null`. usage: provider_mod.Usage = .{}, usage_seen: bool = false, + signature_origin: ?conversation.SignatureOrigin = null, stop_reason: ?[]u8 = null, /// Owned, human-readable description of a mid-stream `error` event /// (e.g. `"overloaded_error: Overloaded"`), surfaced to the agent via @@ -321,6 +340,7 @@ const StreamState = struct { } for (self.blocks.items) |*b| b.deinit(self.allocator); self.blocks.deinit(self.allocator); + if (self.signature_origin) |*o| o.deinit(self.allocator); if (self.stop_reason) |s| self.allocator.free(s); if (self.stream_error_message) |s| self.allocator.free(s); } @@ -557,6 +577,15 @@ const StreamState = struct { defer self.allocator.free(moved_blocks); const usage: ?provider_mod.Usage = if (self.usage_seen) self.usage else null; + if (self.signature_origin) |origin| { + try conversation.setThinkingOrigins( + self.allocator, + moved_blocks, + origin.api_style, + origin.base_url, + origin.model, + ); + } try conv.addAssistantMessage(moved_blocks, usage); const msg = conv.messages.items[conv.messages.items.len - 1]; |
