@@ -306,15 +306,99 @@ fn deinitFn(ptr: *anyopaque) void {
306306 self .deinit ();
307307}
308308
309+ const ServerToolUse = struct {
310+ web_search_requests : ? u64 = null ,
311+ web_fetch_requests : ? u64 = null ,
312+ };
313+
314+ const Usage = struct {
315+ input_tokens : ? u64 = null ,
316+ output_tokens : ? u64 = null ,
317+ cache_read_input_tokens : ? u64 = null ,
318+ cache_creation_input_tokens : ? u64 = null ,
319+ server_tool_use : ? ServerToolUse = null ,
320+ };
321+
322+ const ModelUsageEntry = struct {
323+ inputTokens : ? u64 = null ,
324+ outputTokens : ? u64 = null ,
325+ cacheReadInputTokens : ? u64 = null ,
326+ contextWindow : ? u64 = null ,
327+ maxOutputTokens : ? u64 = null ,
328+ };
329+
330+ const ClaudeJson = struct {
331+ result : ? []const u8 = null ,
332+ is_error : bool = false ,
333+ session_id : ? []const u8 = null ,
334+ duration_ms : ? u64 = null ,
335+ duration_api_ms : ? u64 = null ,
336+ num_turns : ? u64 = null ,
337+ total_cost_usd : ? f64 = null ,
338+ usage : ? Usage = null ,
339+ modelUsage : ? std.json.Value = null ,
340+ };
341+
309342fn parseClaudeResponse (allocator : std.mem.Allocator , line : []const u8 ) ? []const u8 {
310- const parsed = std .json .parseFromSlice (struct {
311- result : ? []const u8 = null ,
312- is_error : bool = false ,
313- }, allocator , line , .{ .ignore_unknown_fields = true }) catch return null ;
343+ const parsed = std .json .parseFromSlice (ClaudeJson , allocator , line , .{ .ignore_unknown_fields = true }) catch return null ;
314344 defer parsed .deinit ();
315345
316- if (parsed .value .is_error ) return null ;
317- const result = parsed .value .result orelse return null ;
346+ const v = parsed .value ;
347+
348+ if (v .is_error ) return null ;
349+
350+ // Log metadata
351+ if (v .total_cost_usd ) | _ | {
352+ const usage = v .usage orelse Usage {};
353+ const in = usage .input_tokens orelse 0 ;
354+ const out = usage .output_tokens orelse 0 ;
355+ const cache_read = usage .cache_read_input_tokens orelse 0 ;
356+ const cache_create = usage .cache_creation_input_tokens orelse 0 ;
357+ const total_tokens = in + out + cache_read + cache_create ;
358+
359+ // Extract context window and model name from modelUsage (first entry)
360+ var context_window : u64 = 0 ;
361+ var model_name : []const u8 = "unknown" ;
362+ if (v .modelUsage ) | mu | {
363+ if (mu == .object ) {
364+ var it = mu .object .iterator ();
365+ if (it .next ()) | entry | {
366+ model_name = entry .key_ptr .* ;
367+ const model_parsed = std .json .parseFromValue (ModelUsageEntry , allocator , entry .value_ptr .* , .{ .ignore_unknown_fields = true }) catch null ;
368+ if (model_parsed ) | mp | {
369+ context_window = mp .value .contextWindow orelse 0 ;
370+ }
371+ }
372+ }
373+ }
374+
375+ const web_searches = if (usage .server_tool_use ) | stu | stu .web_search_requests orelse 0 else 0 ;
376+
377+ if (context_window > 0 ) {
378+ const total_k = total_tokens / 1000 ;
379+ const ctx_k = context_window / 1000 ;
380+ const pct = (total_tokens * 100 ) / context_window ;
381+ std .log .info ("claude: {s} | {d}ms | turns={d} | ctx={d}k/{d}k ({d}%) | web_searches={d}" , .{
382+ model_name ,
383+ v .duration_ms orelse 0 ,
384+ v .num_turns orelse 0 ,
385+ total_k ,
386+ ctx_k ,
387+ pct ,
388+ web_searches ,
389+ });
390+ } else {
391+ std .log .info ("claude: {s} | {d}ms | turns={d} | tokens={d} | web_searches={d}" , .{
392+ model_name ,
393+ v .duration_ms orelse 0 ,
394+ v .num_turns orelse 0 ,
395+ total_tokens ,
396+ web_searches ,
397+ });
398+ }
399+ }
400+
401+ const result = v .result orelse return null ;
318402 const trimmed = std .mem .trim (u8 , result , & std .ascii .whitespace );
319403 if (trimmed .len == 0 ) return null ;
320404
0 commit comments