summaryrefslogtreecommitdiff
path: root/libpanto/src/anthropic_messages_json.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/anthropic_messages_json.zig')
-rw-r--r--libpanto/src/anthropic_messages_json.zig52
1 files changed, 50 insertions, 2 deletions
diff --git a/libpanto/src/anthropic_messages_json.zig b/libpanto/src/anthropic_messages_json.zig
index 708ede5..46ec17c 100644
--- a/libpanto/src/anthropic_messages_json.zig
+++ b/libpanto/src/anthropic_messages_json.zig
@@ -232,9 +232,31 @@ pub const StreamEventTag = enum {
pub const ContentBlockKind = enum { text, thinking, tool_use, unknown };
+/// Partial token-count snapshot emitted in `message_start` and
+/// `message_delta`. Field semantics follow Anthropic's wire format:
+///
+/// - `input_tokens`: prompt tokens billed at the base rate.
+/// - `cache_creation_input_tokens`: prompt tokens written to a new
+/// cache entry (1.25× base).
+/// - `cache_read_input_tokens`: prompt tokens served from cache
+/// (0.1× base).
+/// - `output_tokens`: response tokens billed at the output rate.
+///
+/// `message_start.usage` carries the initial input-side counts (output
+/// will be 0 or absent); `message_delta.usage` carries the final
+/// `output_tokens` and may repeat the input-side counts. The provider
+/// merges them — "missing" means "unchanged," not "reset to zero."
+pub const StreamUsage = struct {
+ input_tokens: ?u64 = null,
+ output_tokens: ?u64 = null,
+ cache_creation_input_tokens: ?u64 = null,
+ cache_read_input_tokens: ?u64 = null,
+};
+
pub const StreamEvent = union(StreamEventTag) {
message_start: struct {
role: ?[]const u8 = null,
+ usage: StreamUsage = .{},
},
content_block_start: struct {
index: usize,
@@ -255,6 +277,7 @@ pub const StreamEvent = union(StreamEventTag) {
},
message_delta: struct {
stop_reason: ?[]const u8 = null,
+ usage: StreamUsage = .{},
},
message_stop: void,
ping: void,
@@ -287,14 +310,18 @@ pub fn parseStreamEvent(allocator: Allocator, payload: []const u8) !ParsedStream
const ty = type_v.string;
if (std.mem.eql(u8, ty, "message_start")) {
var role: ?[]const u8 = null;
+ var usage: StreamUsage = .{};
if (root.object.get("message")) |m| {
if (m == .object) {
if (m.object.get("role")) |r| {
if (r == .string) role = r.string;
}
+ if (m.object.get("usage")) |u| {
+ if (u == .object) usage = parseStreamUsage(u.object);
+ }
}
}
- return .{ .parsed = parsed, .event = .{ .message_start = .{ .role = role } } };
+ return .{ .parsed = parsed, .event = .{ .message_start = .{ .role = role, .usage = usage } } };
}
if (std.mem.eql(u8, ty, "content_block_start")) {
@@ -380,6 +407,7 @@ pub fn parseStreamEvent(allocator: Allocator, payload: []const u8) !ParsedStream
if (std.mem.eql(u8, ty, "message_delta")) {
var stop_reason: ?[]const u8 = null;
+ var usage: StreamUsage = .{};
if (root.object.get("delta")) |d| {
if (d == .object) {
if (d.object.get("stop_reason")) |sr| {
@@ -387,7 +415,11 @@ pub fn parseStreamEvent(allocator: Allocator, payload: []const u8) !ParsedStream
}
}
}
- return .{ .parsed = parsed, .event = .{ .message_delta = .{ .stop_reason = stop_reason } } };
+ // `usage` is on the message_delta event itself, not under `delta`.
+ if (root.object.get("usage")) |u| {
+ if (u == .object) usage = parseStreamUsage(u.object);
+ }
+ return .{ .parsed = parsed, .event = .{ .message_delta = .{ .stop_reason = stop_reason, .usage = usage } } };
}
if (std.mem.eql(u8, ty, "message_stop")) {
@@ -417,6 +449,22 @@ pub fn parseStreamEvent(allocator: Allocator, payload: []const u8) !ParsedStream
return .{ .parsed = parsed, .event = .unknown };
}
+fn parseStreamUsage(obj: std.json.ObjectMap) StreamUsage {
+ return .{
+ .input_tokens = readOptionalU64(obj, "input_tokens"),
+ .output_tokens = readOptionalU64(obj, "output_tokens"),
+ .cache_creation_input_tokens = readOptionalU64(obj, "cache_creation_input_tokens"),
+ .cache_read_input_tokens = readOptionalU64(obj, "cache_read_input_tokens"),
+ };
+}
+
+fn readOptionalU64(obj: std.json.ObjectMap, name: []const u8) ?u64 {
+ const v = obj.get(name) orelse return null;
+ if (v != .integer) return null;
+ if (v.integer < 0) return null;
+ return @intCast(v.integer);
+}
+
fn readIndex(root: std.json.Value) ?usize {
const v = root.object.get("index") orelse return null;
if (v != .integer) return null;