//! Anthropic usage feed implementation. //! //! Mirrors prime-agent:packages/ai/src/utils/usage/providers/anthropic.ts field for field. const std = @import("std"); const usage = @import("../usage.zig"); const iso8601 = @import("../iso8601.zig"); pub const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage"; pub const ANTHROPIC_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile"; const OAUTH_BETA = "oauth-2025-04-20"; pub const WindowInfo = struct { label: []const u8, windowLabel: ?[]const u8 = null, windowSeconds: ?i64 = null, scope: ?[]const u8 = null, }; const five_hours = 5 * 3600; const one_day = 86_400; const one_week = 7 * one_day; /// The part of a kind that names what the window is restricted to; `all` restricts nothing. fn kindScope(scope: []const u8) ?[]const u8 { return if (scope.len == 0 or std.mem.eql(u8, scope, "all")) null else scope; } fn titleCase(alloc: std.mem.Allocator, value: []const u8) ![]const u8 { var parts = std.mem.splitAny(u8, value, "_-"); var res = std.ArrayList(u8).empty; var first = true; while (parts.next()) |part| { if (part.len == 0) continue; if (!first) try res.append(alloc, ' '); first = false; try res.append(alloc, std.ascii.toUpper(part[0])); if (part.len > 1) { try res.appendSlice(alloc, part[1..]); } } return res.items; } pub fn windowForKind(alloc: std.mem.Allocator, kind: []const u8) !WindowInfo { if (std.mem.eql(u8, kind, "session")) { return .{ .label = "Session", .windowLabel = "5h window", .windowSeconds = five_hours }; } if (std.mem.endsWith(u8, kind, "_session")) { const scope = kindScope(kind[0 .. kind.len - "_session".len]); return .{ .label = "Session", .windowLabel = "5h window", .windowSeconds = five_hours, .scope = scope }; } if (std.mem.startsWith(u8, kind, "weekly_")) { const scope = kind["weekly_".len..]; if (std.mem.eql(u8, scope, "all")) { return .{ .label = "Weekly", .windowLabel = "7d window", .windowSeconds = one_week }; } const tc = try titleCase(alloc, scope); const label = try std.fmt.allocPrint(alloc, "Weekly ({s})", .{tc}); return .{ .label = label, .windowLabel = "7d window", .windowSeconds = one_week, .scope = kindScope(scope) }; } if (std.mem.eql(u8, kind, "weekly")) { return .{ .label = "Weekly", .windowLabel = "7d window", .windowSeconds = one_week }; } if (std.mem.startsWith(u8, kind, "daily_")) { const scope = kind["daily_".len..]; const tc = try titleCase(alloc, scope); const label = try std.fmt.allocPrint(alloc, "Daily ({s})", .{tc}); return .{ .label = label, .windowLabel = "24h window", .windowSeconds = one_day, .scope = kindScope(scope) }; } const label = try titleCase(alloc, kind); return .{ .label = label, .windowLabel = null }; } pub const ParseUsageResult = struct { limits: []const usage.UsageLimit, notes: []const []const u8, }; pub fn parseAnthropicUsage(alloc: std.mem.Allocator, payload: ?std.json.Value, _now_ms: i64) !ParseUsageResult { _ = _now_ms; const root = usage.asObject(payload); var notes = std.ArrayList([]const u8).empty; var limits = std.ArrayList(usage.UsageLimit).empty; const entries = if (root) |r| usage.asArray(r.get("limits")) else null; if (entries) |arr| { for (arr.items) |entry| { const limit = usage.asObject(entry) orelse continue; const kind = usage.asString(limit.get("kind")) orelse "unknown"; const used_percent = usage.asNumber(limit.get("percent")); const win = try windowForKind(alloc, kind); const severity = usage.asString(limit.get("severity")); const status = if (used_percent == null) "unknown" else if (severity != null and std.mem.eql(u8, severity.?, "critical")) "warning" else usage.statusFor(used_percent); const final_status = if (used_percent != null and used_percent.? >= 100.0) "exhausted" else status; const resets_at_str = usage.asString(limit.get("resets_at")); const resets_at_ms: ?i64 = if (resets_at_str) |s| iso8601.parseIso8601(s) else null; try limits.append(alloc, .{ .id = kind, .label = win.label, .usedPercent = used_percent, .windowLabel = win.windowLabel, .windowSeconds = win.windowSeconds, .resetsAt = resets_at_ms, // The body's own scope wins over the one read from the kind. .scope = usage.stringOf(limit.get("scope")) orelse win.scope, .status = final_status, }); } } const extra = if (root) |r| usage.asObject(r.get("extra_usage")) else null; if (extra) |ex| { const is_enabled = ex.get("is_enabled"); const enabled_bool = if (is_enabled) |b| switch (b) { .bool => |v| v, else => false, } else false; if (enabled_bool) { const monthly_limit = usage.asNumber(ex.get("monthly_limit")); const used_credits = usage.asNumber(ex.get("used_credits")); const percent: ?f64 = if (monthly_limit == null or monthly_limit.? == 0.0 or used_credits == null) null else (used_credits.? / monthly_limit.?) * 100.0; const detail: ?[]const u8 = if (monthly_limit) |ml| blk: { const uc_int = usage.toInt(i64, used_credits orelse 0.0) orelse break :blk null; const ml_int = usage.toInt(i64, ml) orelse break :blk null; break :blk try std.fmt.allocPrint(alloc, "{d} of {d} credits", .{ uc_int, ml_int }); } else null; try limits.append(alloc, .{ .id = "extra_usage", .label = "Extra usage", .usedPercent = percent, .resetsAt = null, .status = usage.statusFor(percent), .detail = detail, }); } else { const credits_ever_enabled = ex.get("credits_ever_enabled"); const ever_bool = if (credits_ever_enabled) |b| switch (b) { .bool => |v| v, else => false, } else false; if (ever_bool) { try notes.append(alloc, "Extra usage is off"); } } } return ParseUsageResult{ .limits = limits.items, .notes = notes.items, }; } pub fn advance( alloc: std.mem.Allocator, credential_json: []const u8, responses_json: []const u8, now_ms: i64, ) !usage.Outcome { const cred_parsed = std.json.parseFromSlice(std.json.Value, alloc, credential_json, .{}) catch { return .{ .report = .{ .error_message = "invalid credential JSON" } }; }; const cred_root = usage.asObject(cred_parsed.value) orelse { return .{ .report = .{ .error_message = "invalid credential JSON" } }; }; const kind = usage.asString(cred_root.get("kind")) orelse ""; if (!std.mem.eql(u8, kind, "oauth")) { return .{ .report = .{ .error_message = "anthropic needs an OAuth account" } }; } const access = usage.asString(cred_root.get("access")) orelse ""; const cred_email = usage.stringOf(cred_root.get("email")); const cred_account_id = usage.stringOf(cred_root.get("accountId")); const resp_parsed = std.json.parseFromSlice(std.json.Value, alloc, responses_json, .{}) catch { return .{ .report = .{ .error_message = "invalid responses JSON" } }; }; const resp_array = usage.asArray(resp_parsed.value) orelse { return .{ .report = .{ .error_message = "invalid responses JSON" } }; }; if (resp_array.items.len == 0) { var headers = std.ArrayList([2][]const u8).empty; const auth_hdr = try std.fmt.allocPrint(alloc, "Bearer {s}", .{access}); try headers.append(alloc, .{ "Authorization", auth_hdr }); try headers.append(alloc, .{ "anthropic-beta", OAUTH_BETA }); try headers.append(alloc, .{ "User-Agent", "prime-agent" }); const requests = try alloc.alloc(usage.UsageRequest, 2); requests[0] = .{ .method = "GET", .url = ANTHROPIC_USAGE_URL, .headers = headers.items, }; requests[1] = .{ .method = "GET", .url = ANTHROPIC_PROFILE_URL, .headers = headers.items, }; return .{ .requests = requests }; } const first = switch (try usage.parseFirstResponse(alloc, resp_array, "usage body is not JSON")) { .ok => |f| f, .err => |msg| return .{ .report = .{ .error_message = msg } }, }; const body_parsed = first.parsed; // Profile response (optional / responses[1]) var profile_obj: ?std.json.ObjectMap = null; if (resp_array.items.len > 1) { const resp1_obj = usage.asObject(resp_array.items[1]); if (resp1_obj) |r1| { if (usage.asString(r1.get("body"))) |pbody| { if (std.json.parseFromSlice(std.json.Value, alloc, pbody, .{})) |p_parsed| { profile_obj = usage.asObject(p_parsed.value); } else |_| {} } } } const org_obj = if (profile_obj) |p| usage.asObject(p.get("organization")) else null; const acct_obj = if (profile_obj) |p| usage.asObject(p.get("account")) else null; const parsed_usage = try parseAnthropicUsage(alloc, body_parsed.value, now_ms); const plan_type = if (org_obj) |o| (usage.stringOf(o.get("seat_tier")) orelse usage.stringOf(o.get("rate_limit_tier"))) else null; const account_email = if (acct_obj) |a| usage.stringOf(a.get("email")) else null; const final_email: ?[]const u8 = if (account_email) |em| blk: { const lowered = try alloc.alloc(u8, em.len); break :blk std.ascii.lowerString(lowered, em); } else cred_email; const final_account_id = if (cred_account_id) |cid| cid else if (acct_obj) |a| usage.stringOf(a.get("uuid")) else null; return .{ .report = .{ .planType = plan_type, .email = final_email, .accountId = final_account_id, .limits = parsed_usage.limits, .notes = parsed_usage.notes, .error_message = null, }, }; }