//! Pure provider usage query core. //! //! Driven by the host: no sockets, no clock, no filesystem. //! Implements ABI 4 (limit id, windowSeconds and scope). //! The core owns no cross-call state; the host owns it and passes it back. const std = @import("std"); pub const iso8601 = @import("iso8601.zig"); pub const abi_version: u32 = 4; pub const ProviderInfo = struct { provider: []const u8, providerName: []const u8, credentialKinds: []const []const u8, }; pub const supported_providers = [_]ProviderInfo{ .{ .provider = "alibaba", .providerName = "Alibaba Model Studio", .credentialKinds = &[_][]const u8{ "cli", "session" }, }, .{ .provider = "anthropic", .providerName = "Anthropic", .credentialKinds = &[_][]const u8{"oauth"}, }, .{ .provider = "deepseek", .providerName = "DeepSeek", .credentialKinds = &[_][]const u8{"apiKey"}, }, .{ .provider = "google-antigravity", .providerName = "Google Antigravity", .credentialKinds = &[_][]const u8{"oauth"}, }, .{ .provider = "hyper", .providerName = "Charm Hyper", .credentialKinds = &[_][]const u8{ "oauth", "apiKey" }, }, .{ .provider = "openai-codex", .providerName = "ChatGPT Codex", .credentialKinds = &[_][]const u8{"oauth"}, }, .{ .provider = "opencode-go", .providerName = "OpenCode Zen", .credentialKinds = &[_][]const u8{ "oauth", "apiKey" }, }, .{ .provider = "zai", .providerName = "Z.ai", .credentialKinds = &[_][]const u8{"apiKey"}, }, }; /// Cross-round state owned by the host and passed back on each advance call. /// The core owns no cross-call state; the host owns it and passes it back. pub const State = struct { hyper_team_id: ?[]u8 = null, hyper_team_name: ?[]u8 = null, alloc: std.mem.Allocator, pub fn init(alloc: std.mem.Allocator) State { return .{ .alloc = alloc }; } pub fn deinit(self: *State) void { if (self.hyper_team_id) |id| self.alloc.free(id); if (self.hyper_team_name) |name| self.alloc.free(name); self.hyper_team_id = null; self.hyper_team_name = null; } pub fn setHyperTeam(self: *State, team_id: []const u8, team_name: ?[]const u8) !void { if (self.hyper_team_id) |id| self.alloc.free(id); if (self.hyper_team_name) |name| self.alloc.free(name); self.hyper_team_id = try self.alloc.dupe(u8, team_id); self.hyper_team_name = if (team_name) |tn| try self.alloc.dupe(u8, tn) else null; } }; pub const UsageLimit = struct { /// Stable machine key for this limit within its provider; `label` is display text. id: []const u8, label: []const u8, usedPercent: ?f64 = null, windowLabel: ?[]const u8 = null, /// Window length when the provider states it or the plan fixes it; null when unknown or /// calendar-shaped (a month). windowSeconds: ?i64 = null, resetsAt: ?i64 = null, /// The provider's own name for the model family or feature this limit is restricted to; /// null means it counts all usage on the account. scope: ?[]const u8 = null, status: []const u8, detail: ?[]const u8 = null, }; pub const UsageRequestKind = enum { http, command, }; pub const UsageRequest = struct { kind: UsageRequestKind = .http, // http method: []const u8 = "GET", url: []const u8 = "", headers: []const [2][]const u8 = &[_][2][]const u8{}, body: ?[]const u8 = null, // command command: []const u8 = "", args: []const []const u8 = &[_][]const u8{}, stdin: ?[]const u8 = null, timeoutMs: ?i64 = null, }; pub const Outcome = union(enum) { requests: []const UsageRequest, report: ReportOutcome, err: []const u8, }; pub const ReportOutcome = struct { accountId: ?[]const u8 = null, email: ?[]const u8 = null, planType: ?[]const u8 = null, limits: []const UsageLimit = &[_]UsageLimit{}, balance: ?[]const u8 = null, notes: []const []const u8 = &[_][]const u8{}, error_message: ?[]const u8 = null, }; /// Append-only JSON writer over an arena. pub const Writer = struct { buf: std.ArrayList(u8) = .empty, alloc: std.mem.Allocator, pub fn init(alloc: std.mem.Allocator) Writer { return .{ .alloc = alloc }; } pub fn raw(self: *Writer, text: []const u8) !void { try self.buf.appendSlice(self.alloc, text); } pub fn byte(self: *Writer, c: u8) !void { try self.buf.append(self.alloc, c); } pub fn print(self: *Writer, comptime fmt: []const u8, args: anytype) !void { const text = try std.fmt.allocPrint(self.alloc, fmt, args); try self.buf.appendSlice(self.alloc, text); } pub fn string(self: *Writer, value: []const u8) !void { try writeJsonString(self, value); } pub fn items(self: *Writer) []const u8 { return self.buf.items; } }; pub fn writeJsonString(w: *Writer, value: []const u8) !void { try w.byte('"'); for (value) |c| { switch (c) { '"' => try w.raw("\\\""), '\\' => try w.raw("\\\\"), '\n' => try w.raw("\\n"), '\r' => try w.raw("\\r"), '\t' => try w.raw("\\t"), else => { if (c < 0x20) { try w.print("\\u{x:0>4}", .{c}); } else { try w.byte(c); } }, } } try w.byte('"'); } pub fn asObject(value: ?std.json.Value) ?std.json.ObjectMap { const v = value orelse return null; return switch (v) { .object => |o| o, else => null, }; } pub fn asArray(value: ?std.json.Value) ?std.json.Array { const v = value orelse return null; return switch (v) { .array => |a| a, else => null, }; } pub fn asNumber(value: ?std.json.Value) ?f64 { const v = value orelse return null; const f: ?f64 = switch (v) { .integer => |i| @floatFromInt(i), .float => |fl| fl, .number_string => |s| std.fmt.parseFloat(f64, s) catch null, .string => |s| std.fmt.parseFloat(f64, s) catch null, else => null, }; if (f) |num| { if (std.math.isFinite(num)) return num; } return null; } pub fn asString(value: ?std.json.Value) ?[]const u8 { const v = value orelse return null; return switch (v) { .string => |s| s, else => null, }; } pub fn stringOf(value: ?std.json.Value) ?[]const u8 { const s = asString(value) orelse return null; const trimmed = std.mem.trim(u8, s, " \t\r\n"); if (trimmed.len == 0) return null; return trimmed; } pub fn toInt(comptime T: type, val: ?f64) ?T { const v = val orelse return null; if (!std.math.isFinite(v)) return null; const min_f = @as(f64, @floatFromInt(std.math.minInt(T))); const max_f = @as(f64, @floatFromInt(std.math.maxInt(T))); if (v < min_f or v > max_f) return null; return @as(T, @intFromFloat(v)); } pub fn roundToInt(comptime T: type, val: ?f64) ?T { const v = val orelse return null; if (!std.math.isFinite(v)) return null; return toInt(T, @round(v)); } pub fn statusFor(used_percent: ?f64) []const u8 { const used = used_percent orelse return "unknown"; if (!std.math.isFinite(used)) return "unknown"; if (used >= 100) return "exhausted"; if (used >= 90) return "warning"; return "ok"; } pub 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])); for (part[1..]) |c| { try res.append(alloc, std.ascii.toLower(c)); } } return res.items; } pub fn formatWindowLabel(alloc: std.mem.Allocator, seconds: ?f64) !?[]const u8 { const s = seconds orelse return null; if (!std.math.isFinite(s) or !(s > 0)) return null; if (s >= 86_400) { const days = roundToInt(i64, s / 86_400.0) orelse return null; return try std.fmt.allocPrint(alloc, "{d}d window", .{days}); } const hours = roundToInt(i64, s / 3600.0) orelse return null; return try std.fmt.allocPrint(alloc, "{d}h window", .{if (hours < 1) 1 else hours}); } pub fn epochToWindowLabel(alloc: std.mem.Allocator, resets_at_ms: ?i64, now_ms: i64) !?[]const u8 { const ra = resets_at_ms orelse return null; // A reset already in the past floors to the 1h label rather than disappearing: the reference // (prime-agent's driver.ts epochToWindowLabel) does `Math.max(1, …)`, and a host that shows // nothing would look like a provider with no window at all. const diff = ra - now_ms; const diff_f = @as(f64, @floatFromInt(diff)) / 1000.0; const seconds = @max(1, roundToInt(i64, diff_f) orelse return null); if (seconds >= 86_400) { const days = roundToInt(i64, @as(f64, @floatFromInt(seconds)) / 86_400.0) orelse return null; return try std.fmt.allocPrint(alloc, "{d}d window", .{days}); } const hours = @max(1, roundToInt(i64, @as(f64, @floatFromInt(seconds)) / 3600.0) orelse return null); return try std.fmt.allocPrint(alloc, "{d}h window", .{hours}); } pub fn httpStatusError(alloc: std.mem.Allocator, response_obj: ?std.json.ObjectMap) ![]const u8 { const ro = response_obj orelse return "HTTP no response"; const status_num = asNumber(ro.get("status")); const status = toInt(i64, status_num) orelse return "HTTP no response"; const body_str = asString(ro.get("body")); if (body_str) |bs| { if (std.json.parseFromSlice(std.json.Value, alloc, bs, .{})) |parsed| { if (asObject(parsed.value)) |root| { if (root.get("error")) |err_val| { if (asString(err_val)) |err_str| { const trimmed = std.mem.trim(u8, err_str, " \t\r\n"); if (trimmed.len > 0) { return try std.fmt.allocPrint(alloc, "HTTP {d}: {s}", .{ status, trimmed }); } } else if (asObject(err_val)) |err_obj| { if (asString(err_obj.get("message"))) |msg| { const trimmed = std.mem.trim(u8, msg, " \t\r\n"); if (trimmed.len > 0) { return try std.fmt.allocPrint(alloc, "HTTP {d}: {s}", .{ status, trimmed }); } } } } } } else |_| {} } return try std.fmt.allocPrint(alloc, "HTTP {d}", .{status}); } pub const FirstResponse = struct { obj: std.json.ObjectMap, parsed: std.json.Parsed(std.json.Value), }; pub const FirstResponseOutcome = union(enum) { ok: FirstResponse, err: []const u8, }; pub fn parseFirstResponse( alloc: std.mem.Allocator, resp_array: std.json.Array, not_json_msg: []const u8, ) !FirstResponseOutcome { return parseFirstResponseEx(alloc, resp_array, not_json_msg, false); } pub fn parseFirstResponseEx( alloc: std.mem.Allocator, resp_array: std.json.Array, not_json_msg: []const u8, unwrap_http_error: bool, ) !FirstResponseOutcome { if (resp_array.items.len == 0) return .{ .err = "HTTP no response" }; const resp0_obj = asObject(resp_array.items[0]) orelse return .{ .err = "HTTP no response" }; const status_num = asNumber(resp0_obj.get("status")); const status = toInt(i64, status_num); if (status == null or status.? != 200) { if (unwrap_http_error) { return .{ .err = try httpStatusError(alloc, resp0_obj) }; } const err_text = if (status) |st| try std.fmt.allocPrint(alloc, "HTTP {d}", .{st}) else "HTTP no response"; return .{ .err = err_text }; } const body_str = asString(resp0_obj.get("body")) orelse return .{ .err = not_json_msg }; const parsed = std.json.parseFromSlice(std.json.Value, alloc, body_str, .{}) catch { return .{ .err = not_json_msg }; }; return .{ .ok = .{ .obj = resp0_obj, .parsed = parsed } }; } pub fn urlEncode(alloc: std.mem.Allocator, input: []const u8) ![]const u8 { var out = std.ArrayList(u8).empty; const hex = "0123456789ABCDEF"; for (input) |c| { switch (c) { 'a'...'z', 'A'...'Z', '0'...'9', '-', '_', '.', '*' => try out.append(alloc, c), ' ' => try out.append(alloc, '+'), else => { try out.append(alloc, '%'); try out.append(alloc, hex[(c >> 4) & 0x0F]); try out.append(alloc, hex[c & 0x0F]); }, } } return out.items; } pub fn writeStep(w: *Writer, outcome: Outcome) !void { switch (outcome) { .requests => |reqs| { try w.raw("{\"requests\":["); for (reqs, 0..) |req, i| { if (i > 0) try w.byte(','); switch (req.kind) { .http => { try w.raw("{\"kind\":\"http\",\"method\":"); try w.string(req.method); try w.raw(",\"url\":"); try w.string(req.url); try w.raw(",\"headers\":["); for (req.headers, 0..) |h, hi| { if (hi > 0) try w.byte(','); try w.raw("["); try w.string(h[0]); try w.byte(','); try w.string(h[1]); try w.raw("]"); } try w.raw("]"); if (req.body) |body| { try w.raw(",\"body\":"); try w.string(body); } try w.byte('}'); }, .command => { try w.raw("{\"kind\":\"command\",\"command\":"); try w.string(req.command); try w.raw(",\"args\":["); for (req.args, 0..) |arg, ai| { if (ai > 0) try w.byte(','); try w.string(arg); } try w.byte(']'); if (req.stdin) |st| { try w.raw(",\"stdin\":"); try w.string(st); } if (req.timeoutMs) |to| { try w.print(",\"timeoutMs\":{d}", .{to}); } try w.byte('}'); }, } } try w.raw("]}"); }, .report => |rep| { try w.raw("{\"report\":{"); var wrote = false; if (rep.accountId) |aid| { try w.raw("\"accountId\":"); try w.string(aid); wrote = true; } if (rep.email) |em| { if (wrote) try w.byte(','); try w.raw("\"email\":"); try w.string(em); wrote = true; } if (rep.planType) |pt| { if (wrote) try w.byte(','); try w.raw("\"planType\":"); try w.string(pt); wrote = true; } if (wrote) try w.byte(','); try w.raw("\"limits\":["); for (rep.limits, 0..) |lim, i| { if (i > 0) try w.byte(','); try w.raw("{\"id\":"); try w.string(lim.id); try w.raw(",\"label\":"); try w.string(lim.label); if (lim.usedPercent) |up| { if (std.math.isFinite(up)) { const clamped = @max(0.0, @min(100.0, up)); try w.print(",\"usedPercent\":{d}", .{clamped}); } } if (lim.windowLabel) |wl| { try w.raw(",\"windowLabel\":"); try w.string(wl); } if (lim.windowSeconds) |ws| { try w.print(",\"windowSeconds\":{d}", .{ws}); } if (lim.resetsAt) |ra| { try w.print(",\"resetsAt\":{d}", .{ra}); } if (lim.scope) |sc| { try w.raw(",\"scope\":"); try w.string(sc); } if (lim.detail) |dt| { try w.raw(",\"detail\":"); try w.string(dt); } try w.raw(",\"status\":"); try w.string(lim.status); try w.byte('}'); } try w.byte(']'); wrote = true; if (rep.balance) |bal| { try w.raw(",\"balance\":"); try w.string(bal); } if (rep.notes.len > 0) { try w.raw(",\"notes\":["); for (rep.notes, 0..) |note, ni| { if (ni > 0) try w.byte(','); try w.string(note); } try w.byte(']'); } try w.raw(",\"error\":"); if (rep.error_message) |err_msg| { try w.string(err_msg); } else { try w.raw("null"); } try w.raw("}}"); }, .err => |msg| { try w.raw("{\"error\":"); try w.string(msg); try w.raw("}"); }, } } pub fn writeProviders(w: *Writer) !void { try w.raw("["); for (supported_providers, 0..) |p, i| { if (i > 0) try w.byte(','); try w.raw("{\"provider\":"); try w.string(p.provider); try w.raw(",\"providerName\":"); try w.string(p.providerName); try w.raw(",\"credentialKinds\":["); for (p.credentialKinds, 0..) |k, ki| { if (ki > 0) try w.byte(','); try w.string(k); } try w.raw("]}"); } try w.raw("]"); } // Re-export providers pub const codex = @import("providers/codex.zig"); pub const anthropic = @import("providers/anthropic.zig"); pub const antigravity = @import("providers/antigravity.zig"); pub const hyper = @import("providers/hyper.zig"); pub const opencode_go = @import("providers/opencode_go.zig"); pub const zai = @import("providers/zai.zig"); pub const deepseek = @import("providers/deepseek.zig"); pub const alibaba = @import("providers/alibaba.zig"); const ProviderDispatch = struct { name: []const u8, advance: *const fn (std.mem.Allocator, ?*State, []const u8, []const u8, i64) anyerror!Outcome, }; fn wrapStateless(comptime func: anytype) *const fn (std.mem.Allocator, ?*State, []const u8, []const u8, i64) anyerror!Outcome { return struct { fn call(alloc: std.mem.Allocator, state: ?*State, cred: []const u8, resp: []const u8, now_ms: i64) anyerror!Outcome { _ = state; return func(alloc, cred, resp, now_ms); } }.call; } const provider_table = [_]ProviderDispatch{ .{ .name = "alibaba", .advance = wrapStateless(alibaba.advance) }, .{ .name = "anthropic", .advance = wrapStateless(anthropic.advance) }, .{ .name = "deepseek", .advance = wrapStateless(deepseek.advance) }, .{ .name = "google-antigravity", .advance = wrapStateless(antigravity.advance) }, .{ .name = "hyper", .advance = hyper.advance }, .{ .name = "openai-codex", .advance = wrapStateless(codex.advance) }, .{ .name = "opencode-go", .advance = wrapStateless(opencode_go.advance) }, .{ .name = "zai", .advance = wrapStateless(zai.advance) }, }; pub fn advance( alloc: std.mem.Allocator, state: ?*State, provider: []const u8, credential_json: []const u8, responses_json: []const u8, now_ms: i64, ) !Outcome { inline for (provider_table) |entry| { if (std.mem.eql(u8, provider, entry.name)) { return entry.advance(alloc, state, credential_json, responses_json, now_ms); } } return .{ .err = "unknown provider" }; } pub const ClaimsOutcome = union(enum) { ok, err: []const u8 }; pub fn claimsFromAccessToken(alloc: std.mem.Allocator, jwt: []const u8, w: *Writer) !ClaimsOutcome { const payload = jwtPayload(alloc, jwt) catch return .{ .err = "access token is not a JWT" }; defer alloc.free(payload); var parsed = std.json.parseFromSlice(std.json.Value, alloc, payload, .{}) catch { return .{ .err = "access token claims are not JSON" }; }; defer parsed.deinit(); const root = asObject(parsed.value) orelse return .{ .err = "access token claims are not an object" }; const auth = asObject(root.get("https://api.openai.com/auth")); const profile = asObject(root.get("https://api.openai.com/profile")); const account_id = if (auth) |a| asString(a.get("chatgpt_account_id")) else null; const email_claim = if (profile) |p| asString(p.get("email")) else null; const email: ?[]const u8 = if (email_claim) |raw| blk: { const trimmed = std.mem.trim(u8, raw, " \t\r\n"); if (trimmed.len == 0) break :blk null; const lowered = try alloc.alloc(u8, trimmed.len); break :blk std.ascii.lowerString(lowered, trimmed); } else null; try w.raw("{"); var wrote = false; if (account_id) |id| { try w.raw("\"accountId\":"); try w.string(id); wrote = true; } if (email) |value| { if (wrote) try w.byte(','); try w.raw("\"email\":"); try w.string(value); } try w.byte('}'); return .ok; } pub fn jwtPayload(alloc: std.mem.Allocator, jwt: []const u8) ![]u8 { var parts = std.mem.splitScalar(u8, jwt, '.'); _ = parts.next() orelse return error.NotAJwt; const encoded = parts.next() orelse return error.NotAJwt; _ = parts.next() orelse return error.NotAJwt; const decoder = std.base64.url_safe_no_pad.Decoder; const size = try decoder.calcSizeForSlice(encoded); const decoded = try alloc.alloc(u8, size); errdefer alloc.free(decoded); try decoder.decode(decoded, encoded); return decoded; }