summaryrefslogtreecommitdiff
path: root/libpanto/src/provider_anthropic_messages.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/provider_anthropic_messages.zig')
-rw-r--r--libpanto/src/provider_anthropic_messages.zig37
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];