Something went wrong. Try again.
zig library for building requests to query inference providers usage limits
Something went wrong. Try again.
12 kB · 289 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290//! Charm Hyper usage feed implementation.//!//! Mirrors prime-agent:packages/ai/src/utils/usage/providers/hyper.ts field for field.const std = @import("std");const usage = @import("../usage.zig");const iso8601 = @import("../iso8601.zig");
pub const HYPER_TEAMS_URL = "https://hyper.charm.land/v1/teams";
pub const ParseBillingResult = struct { planType: ?[]const u8 = null, limits: []const usage.UsageLimit = &[_]usage.UsageLimit{}, balance: ?[]const u8 = null,};
pub fn parseHyperBilling( alloc: std.mem.Allocator, payload: ?std.json.Value, now_ms: i64,) !ParseBillingResult { const root = usage.asObject(payload); const plan = if (root) |r| usage.asObject(r.get("plan")) else null; const balances = if (root) |r| usage.asObject(r.get("balances")) else null; const plan_balances = if (balances) |b| usage.asObject(b.get("plan")) else null; const total_balances = if (balances) |b| usage.asObject(b.get("total")) else null; const refresh = if (root) |r| usage.asObject(r.get("hypercredit_refresh")) else null;
const plan_name = if (plan) |p| usage.asString(p.get("name")) else null; const plan_type_raw = if (plan) |p| usage.asString(p.get("type")) else null;
const plan_type: ?[]const u8 = if (plan_name) |pn| blk: { const trimmed = std.mem.trim(u8, pn, " \t\r\n"); if (trimmed.len == 0) break :blk null; const lower = try alloc.alloc(u8, trimmed.len); break :blk std.ascii.lowerString(lower, trimmed); } else if (plan_type_raw) |ptr| blk: { const trimmed = std.mem.trim(u8, ptr, " \t\r\n"); if (trimmed.len == 0) break :blk null; const lower = try alloc.alloc(u8, trimmed.len); break :blk std.ascii.lowerString(lower, trimmed); } else null;
const limit = if (plan) |p| usage.asNumber(p.get("hypercredits")) else null; const remaining = if (plan_balances) |pb| usage.asNumber(pb.get("hypercredits")) else null;
var used_percent: ?f64 = null; if (limit != null and limit.? > 0.0 and remaining != null) { const used = @max(0.0, limit.? - remaining.?); used_percent = @max(0.0, @min(100.0, @round((used / limit.?) * 1000.0) / 10.0)); } else if (remaining != null and remaining.? <= 0.0) { used_percent = 100.0; }
const resets_at: ?i64 = if (refresh) |rf| (if (usage.asString(rf.get("next_at"))) |s| iso8601.parseIso8601(s) else null) else null; const last_at: ?i64 = if (refresh) |rf| (if (usage.asString(rf.get("last_at"))) |s| iso8601.parseIso8601(s) else null) else null;
var window_label: ?[]const u8 = null; var window_seconds: ?i64 = null; if (resets_at != null and last_at != null) { const diff_ms = resets_at.? - last_at.?; if (diff_ms > 0) { const diff_sec = usage.roundToInt(i64, @as(f64, @floatFromInt(diff_ms)) / 1000.0) orelse 0; // The refresh period is measured from its own endpoints, so it is the window. if (diff_sec > 0) window_seconds = diff_sec; if (diff_sec >= 86_400) { const days = usage.roundToInt(i64, @as(f64, @floatFromInt(diff_sec)) / 86_400.0) orelse 1; window_label = if (days == 1) "24h window" else try std.fmt.allocPrint(alloc, "{d}d window", .{days}); } else if (diff_sec > 0) { const hours = @max(1, usage.roundToInt(i64, @as(f64, @floatFromInt(diff_sec)) / 3600.0) orelse 1); window_label = try std.fmt.allocPrint(alloc, "{d}h window", .{hours}); } } } else { window_label = try usage.epochToWindowLabel(alloc, resets_at, now_ms); }
const root_status = if (root) |r| usage.asString(r.get("status")) else null; const status: []const u8 = if (root_status == null or !std.mem.eql(u8, root_status.?, "active")) "unknown" else if (used_percent == null) "unknown" else if (used_percent.? >= 100.0) "exhausted" else if (used_percent.? >= 90.0) "warning" else "ok";
const detail: ?[]const u8 = if (remaining != null and limit != null) blk: { const rem = usage.toInt(i64, remaining.?) orelse break :blk null; const lim = usage.toInt(i64, limit.?) orelse break :blk null; break :blk try std.fmt.allocPrint(alloc, "{d} of {d} credits", .{ rem, lim }); } else null;
var limits = std.ArrayList(usage.UsageLimit).empty; try limits.append(alloc, .{ .id = "hypercredits", .label = "Primary", .usedPercent = used_percent, .windowLabel = window_label, .windowSeconds = window_seconds, .resetsAt = resets_at, .status = status, .detail = detail, });
const balance: ?[]const u8 = if (total_balances) |tb| blk: { if (usage.asString(tb.get("usd"))) |u| { const trimmed = std.mem.trim(u8, u, " \t\r\n"); if (trimmed.len > 0) break :blk trimmed; } if (usage.asNumber(tb.get("usd"))) |n| { break :blk try std.fmt.allocPrint(alloc, "{d:.2}", .{n}); } break :blk null; } else null;
return ParseBillingResult{ .planType = plan_type, .limits = limits.items, .balance = balance, };}
pub fn advance( alloc: std.mem.Allocator, state: ?*usage.State, 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 ""; const is_oauth = std.mem.eql(u8, kind, "oauth"); const is_api_key = std.mem.eql(u8, kind, "apiKey");
if (!is_oauth and !is_api_key) { return .{ .report = .{ .error_message = "hyper needs an apiKey or OAuth credential" } }; }
const token = if (is_oauth) usage.asString(cred_root.get("access")) orelse "" else usage.asString(cred_root.get("apiKey")) orelse "";
const cred_account_id = usage.stringOf(cred_root.get("accountId")); const cred_email = usage.stringOf(cred_root.get("email"));
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" } }; };
var auth_hdr_list = std.ArrayList([2][]const u8).empty; const auth_value = try std.fmt.allocPrint(alloc, "Bearer {s}", .{token}); try auth_hdr_list.append(alloc, .{ "Authorization", auth_value });
// Round 1: Fetch teams if (resp_array.items.len == 0) { const requests = try alloc.alloc(usage.UsageRequest, 1); requests[0] = .{ .method = "GET", .url = HYPER_TEAMS_URL, .headers = auth_hdr_list.items, }; return .{ .requests = requests }; }
// Round 2: the answer is either the teams list or already a billing body. The reference // (hyper.ts) distinguishes them by shape, not by round number, so a caller that hands the // billing body in as the only response gets a report instead of "no teams found". const first_payload = switch (try usage.parseFirstResponse(alloc, resp_array, "teams body is not JSON")) { .ok => |f| f, .err => |msg| return .{ .report = .{ .error_message = msg } }, }; const first_object = usage.asObject(first_payload.parsed.value); const is_billing_response = first_object != null and (first_object.?.get("plan") != null or first_object.?.get("balances") != null);
if (!is_billing_response and resp_array.items.len == 1) { const teams_root = first_object; const items = if (teams_root) |tr| usage.asArray(tr.get("items")) else null; if (items == null or items.?.items.len == 0) { return .{ .report = .{ .error_message = "no hyper teams found" } }; }
var selected_team: ?std.json.ObjectMap = null; if (cred_account_id) |caid| { for (items.?.items) |it| { const o = usage.asObject(it) orelse continue; if (usage.asString(o.get("id"))) |id| { if (std.mem.eql(u8, id, caid)) { selected_team = o; break; } } } } if (selected_team == null) { selected_team = usage.asObject(items.?.items[0]); }
const team_id = if (selected_team) |st| usage.asString(st.get("id")) else null; const team_name = if (selected_team) |st| usage.asString(st.get("name")) else null;
if (team_id == null or team_id.?.len == 0) { return .{ .report = .{ .error_message = "invalid team in hyper response" } }; }
if (state) |s| { try s.setHyperTeam(team_id.?, team_name); }
const billing_url = try std.fmt.allocPrint(alloc, "{s}/{s}/billing", .{ HYPER_TEAMS_URL, team_id.? }); const members_url = try std.fmt.allocPrint(alloc, "{s}/{s}/members", .{ HYPER_TEAMS_URL, team_id.? });
const requests = try alloc.alloc(usage.UsageRequest, 2); requests[0] = .{ .method = "GET", .url = billing_url, .headers = auth_hdr_list.items, }; requests[1] = .{ .method = "GET", .url = members_url, .headers = auth_hdr_list.items, }; return .{ .requests = requests }; }
// Round 3: Billing & members responses handling. The billing body is whichever response the // shaping above identified, so it is already parsed. const parsed_billing = try parseHyperBilling(alloc, first_payload.parsed.value, now_ms);
var member_email: ?[]const u8 = null; if (resp_array.items.len >= 2) { const resp1_obj = usage.asObject(resp_array.items[1]); if (resp1_obj) |r1| { const st1 = usage.toInt(i64, usage.asNumber(r1.get("status"))); if (st1 != null and st1.? == 200) { if (usage.asString(r1.get("body"))) |b1_str| { if (std.json.parseFromSlice(std.json.Value, alloc, b1_str, .{})) |b1_parsed| { if (usage.asObject(b1_parsed.value)) |b1_obj| { if (usage.asArray(b1_obj.get("items"))) |mitems| { if (mitems.items.len > 0) { if (usage.asObject(mitems.items[0])) |m0| { if (usage.asString(m0.get("email"))) |em| { const trimmed = std.mem.trim(u8, em, " \t\r\n"); if (trimmed.len > 0) member_email = trimmed; } } } } } } else |_| {} } } } }
const cached_id = if (state) |s| s.hyper_team_id else null; const cached_name = if (state) |s| s.hyper_team_name else null; const account_id = cred_account_id orelse cached_id; const email = member_email orelse cred_email; const notes: []const []const u8 = if (cached_name) |tn| blk: { const arr = try alloc.alloc([]const u8, 1); arr[0] = try std.fmt.allocPrint(alloc, "Team: {s}", .{tn}); break :blk arr; } else &[_][]const u8{};
return .{ .report = .{ .planType = parsed_billing.planType, .accountId = account_id, .email = email, .limits = parsed_billing.limits, .balance = parsed_billing.balance, .notes = notes, .error_message = null, }, };}