feat(native): every provider in the zig core, plus the swift wrapper a host can call
The core could report one provider. It now covers all eight of them from the same definitions the typescript side uses, which is what makes the tray able to drop its own six fetchers. - `usage_advance` (ABI 2) is the host-driven protocol: hand it the credential and whatever responses you already have, get back either the requests to send or the report. hyper's teams-then-billing is just another round, not a second concept. - providers/: antigravity (pools 33 model buckets into Gemini/Claude/GPT-OSS families, drops the internal `chat_*`/`tab_*` ones, and keeps the `antigravity/...` user-agent its endpoint gates on), hyper (credits allocated vs remaining, dollar balance), opencode-go, zai, deepseek (balance-only), alibaba (three console-gateway calls behind a captured session), plus the codex and anthropic ports. - `iso8601.zig`: Zig's std has no parser and four providers hand back ISO strings, with and without fractional seconds and with `Z` or `±hh:mm` offsets. Its unit tests were recomputed independently. - `packages/ai/test/usage-zig-parity.test.ts` now compares all eight providers against the typescript feeds on the recorded bodies, deeply, so a conversion that drifts on either side fails the build. The child proved the comparison bites: dropping the remaining-fraction inversion in antigravity turns 83.4% into 16.6% and fails the antigravity case. - `swift/` is a SwiftPM package: `UsageCore` (the C bridge, with strings freed and NULL turned into a thrown error), `UsageDriver` (the host loop over URLSession, where a transport failure becomes a response with status 0 so the feed reports it as data), and `usage-smoke`, which pins the things most likely to be "fixed" wrongly: codex's three windows, anthropic's ISO reset becoming epoch milliseconds, antigravity's 83.4% inversion and its user-agent requirement, and alibaba's three calls carrying the captured cookies, csrf token and region in the form body. - `npm run check:zig` builds and runs the Swift smoke test where a swift toolchain exists, and says so when it skips. Verified: zig unit + C ABI tests, the eight-provider parity suite, the Swift smoke test against the recorded fixtures, and npm run check end to end.
63 unmodified lines646566676869707172737475767778798081828384858687888990919293949596975 unmodified lines63 unmodified lines`tests/fixtures/` are recorded provider bodies; the live one was captured from`chatgpt.com/backend-api/wham/usage` with the `user_id`, `account_id`, and `email` fieldsremoved.
## Swift
`swift/` wraps this library for native macOS hosts (dawn's tray app is the first consumer):
```shzig build # produces zig-out/lib/libusage.aswift build --package-path native/usage/swiftswift run --package-path native/usage/swift usage-smoke```
- `UsageCore` is the thin C bridge: `providers()`, `advance(provider:credentialJSON:responses:nowMs:)` returning either the requests or the report, and `credentialsFromAccessToken`. Every returned string is freed with `usage_free`, and a NULL return becomes a thrown error rather than a crash.- `UsageDriver` runs the host loop with `URLSession`: ask the core for requests, send them, hand the responses back, repeat. A transport failure comes back as a response with status `0`, so the feed reports it as data and the row shows the reason instead of the call throwing.- `usage-smoke` proves the path against the recorded fixtures with no network: ABI version, provider list, the codex request shape, the codex report, anthropic's ISO reset time becoming epoch milliseconds, the warning status, the plan type from the profile, and the unknown-provider error.
Headers cross the ABI as ordered pairs because HTTP allows a repeated name; `UsageRequest.headers`is the dictionary view for the common case.
The library is built for the host's deployment target. For a macOS 13 app, build it with`zig build -Dtarget=aarch64-macos.13`.
## Status
- One provider has a feed: `openai-codex`. Others return `{"error":"provider has no usage feed"}`, which is how a host names providers that have5 unmodified lines12345567898 unmodified lines1819202122222324252627282930313233343536373839404142434431323334353637383940414243454647481 unmodified line/* Provider usage query core - C ABI. * * The core is pure: it never opens a socket, reads a clock, or touches the * filesystem. The host does the I/O, so a macOS app can use NSURLSession (system * trust store, proxy support, HTTP/2) and hand the body back for parsing. * trust store, proxy support, HTTP/2) and hand the body back. * * Every function returns a NUL-terminated JSON string owned by the library, or * NULL when allocation failed. Failures are reported inside the JSON as * {"error":"..."} so a caller never has to distinguish "no error" from "no data".8 unmodified lines#ifdef __cplusplusextern "C" {#endif
/* ABI version. Bump on any change to the exported signatures or their shapes. *//* ABI version. Bumped to 2 for host-driven requests/report protocol. */uint32_t usage_abi_version(void);
/* Release a string returned by this library. NULL is accepted. */void usage_free(char *ptr);
/* Provider definitions: [{"provider","providerName","credentialKinds"}] */char *usage_providers(void);
/* Advance feed state. * Returns either: * {"requests":[{"method","url","headers":[["k","v"]],"body"?}]} * or: * {"report":{providerName, accountId?, email?, planType?, limits[], balance?, notes?[], error?}} */char *usage_advance(const char *provider, size_t provider_len, const char *credential_json, size_t credential_len, const char *responses_json, size_t responses_len, int64_t now_ms);
/* {"accountId"?: string, "email"?: string} from an OAuth access token's claims. */char *usage_credentials_from_access_token(const char *jwt, size_t jwt_len);
/* {"url": string, "headers": [[name, value], ...]} for a provider's usage request. * account_id may be NULL. */char *usage_build_request(const char *provider, size_t provider_len, const char *access_token, size_t access_token_len, const char *account_id, size_t account_id_len);
/* {"planType"?: string, "limits": [{label, usedPercent?, windowLabel?, resetsAt?, status}]} */char *usage_parse(const char *provider, size_t provider_len, const char *body, size_t body_len, int64_t now_ms);
/* Provider ids with a usage feed. */char *usage_supported_providers(void);
#ifdef __cplusplus}#endif
1 unmodified line40 unmodified lines4142434445454647484950515253545556515253546055565763645859606162636465666771726869707172737475767778797581828384857687777879909192938081829697989910010110210383848586871088811089909192933 unmodified lines40 unmodified linesexport fn usage_free(ptr: ?[*:0]u8) void { if (ptr) |p| alloc.free(std.mem.span(p));}
export fn usage_supported_providers() ?[*:0]u8 {export fn usage_providers() ?[*:0]u8 { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const a = arena.allocator();
var w = usage.Writer.init(a); w.raw("[") catch return null; for (usage.supported_providers, 0..) |provider, i| { if (i > 0) w.raw(",") catch return null; w.string(provider) catch return null; } w.raw("]") catch return null; usage.writeProviders(&w) catch return null; return finish(&w);}
export fn usage_parse(export fn usage_advance( provider: ?[*]const u8, provider_len: usize, body: ?[*]const u8, body_len: usize, credential_json: ?[*]const u8, credential_len: usize, responses_json: ?[*]const u8, responses_len: usize, now_ms: i64,) ?[*:0]u8 { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const a = arena.allocator();
var w = usage.Writer.init(a); const outcome = usage.parse(a, slice(provider, provider_len), slice(body, body_len), now_ms, &w) catch |e| { const prov_slice = slice(provider, provider_len); const cred_slice = slice(credential_json, credential_len); const resp_slice = slice(responses_json, responses_len);
const outcome = usage.advance(a, prov_slice, cred_slice, resp_slice, now_ms) catch |e| { return errorResult(a, message(e)); }; switch (outcome) { .ok => return finish(&w), .err => |text| return errorResult(a, text), }}
export fn usage_credentials_from_access_token(jwt: ?[*]const u8, jwt_len: usize) ?[*:0]u8 { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const a = arena.allocator();
var w = usage.Writer.init(a); const outcome = usage.claimsFromAccessToken(a, slice(jwt, jwt_len), &w) catch |e| { usage.writeStep(&w, outcome) catch |e| { return errorResult(a, message(e)); }; switch (outcome) { .ok => return finish(&w), .err => |text| return errorResult(a, text), } return finish(&w);}
export fn usage_build_request( provider: ?[*]const u8, provider_len: usize, access_token: ?[*]const u8, access_token_len: usize, account_id: ?[*]const u8, account_id_len: usize,) ?[*:0]u8 {export fn usage_credentials_from_access_token(jwt: ?[*]const u8, jwt_len: usize) ?[*:0]u8 { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const a = arena.allocator();
const account_slice: ?[]const u8 = if (account_id) |id| id[0..account_id_len] else null; var w = usage.Writer.init(a); const outcome = usage.buildRequest(slice(provider, provider_len), slice(access_token, access_token_len), account_slice, &w) catch |e| { const outcome = usage.claimsFromAccessToken(a, slice(jwt, jwt_len), &w) catch |e| { return errorResult(a, message(e)); }; switch (outcome) { .ok => return finish(&w),3 unmodified lines123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127//! ISO-8601 parser for usage feed reset timestamps.//!//! Handles full ISO-8601 strings, e.g.://! - 2026-09-16T08:00:00.037593+00:00//! - 2026-09-15T23:46:00Z//! - 2026-06-09T11:28:39.481128Z//! - 2026-01-01T00:00:00+05:30//! - 2026-01-01T00:00:00-08:00//!//! Returns epoch milliseconds (UTC), matching Date.parse in TypeScript.const std = @import("std");
fn isLeapYear(year: i64) bool { if (@mod(year, 4) != 0) return false; if (@mod(year, 100) != 0) return true; return @mod(year, 400) == 0;}
const days_before_month_common = [_]i64{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };const days_before_month_leap = [_]i64{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 };
pub fn daysFromCivil(year: i64, month: u8, day: u8) i64 { // Days from 1970-01-01 to start of `year` var leaps: i64 = 0; if (year >= 1970) { const y_prev = year - 1; leaps = @divFloor(y_prev, 4) - @divFloor(y_prev, 100) + @divFloor(y_prev, 400) - (@divFloor(1969, 4) - @divFloor(1969, 100) + @divFloor(1969, 400)); } else { const y_curr = year; leaps = @divFloor(1969, 4) - @divFloor(1969, 100) + @divFloor(1969, 400) - (@divFloor(y_curr - 1, 4) - @divFloor(y_curr - 1, 100) + @divFloor(y_curr - 1, 400)); leaps = -leaps; } const days_in_years = (year - 1970) * 365 + leaps; const m_days = if (isLeapYear(year)) days_before_month_leap[month - 1] else days_before_month_common[month - 1]; return days_in_years + m_days + (day - 1);}
pub fn parseIso8601(str: []const u8) ?i64 { const trimmed = std.mem.trim(u8, str, " \t\r\n"); if (trimmed.len < 19) return null; // YYYY-MM-DDTHH:MM:SS if (trimmed[4] != '-' or trimmed[7] != '-' or (trimmed[10] != 'T' and trimmed[10] != 't' and trimmed[10] != ' ')) return null; if (trimmed[13] != ':' or trimmed[16] != ':') return null;
const year = std.fmt.parseInt(i64, trimmed[0..4], 10) catch return null; const month = std.fmt.parseInt(u8, trimmed[5..7], 10) catch return null; const day = std.fmt.parseInt(u8, trimmed[8..10], 10) catch return null; if (month < 1 or month > 12) return null; if (day < 1 or day > 31) return null;
const hour = std.fmt.parseInt(i64, trimmed[11..13], 10) catch return null; const minute = std.fmt.parseInt(i64, trimmed[14..16], 10) catch return null; const second = std.fmt.parseInt(i64, trimmed[17..19], 10) catch return null; if (hour < 0 or hour > 23 or minute < 0 or minute > 59 or second < 0 or second > 60) return null;
var idx: usize = 19; var millis: i64 = 0; if (idx < trimmed.len and trimmed[idx] == '.') { idx += 1; var frac_digits: usize = 0; var frac_val: i64 = 0; while (idx < trimmed.len and std.ascii.isDigit(trimmed[idx])) : (idx += 1) { if (frac_digits < 3) { frac_val = frac_val * 10 + (trimmed[idx] - '0'); frac_digits += 1; } } while (frac_digits < 3) : (frac_digits += 1) { frac_val *= 10; } millis = frac_val; }
var offset_sec: i64 = 0; if (idx < trimmed.len) { if (trimmed[idx] == 'Z' or trimmed[idx] == 'z') { idx += 1; } else if (trimmed[idx] == '+' or trimmed[idx] == '-') { const sign: i64 = if (trimmed[idx] == '+') 1 else -1; idx += 1; if (idx + 2 <= trimmed.len) { const off_h = std.fmt.parseInt(i64, trimmed[idx .. idx + 2], 10) catch return null; idx += 2; var off_m: i64 = 0; if (idx < trimmed.len and trimmed[idx] == ':') idx += 1; if (idx + 2 <= trimmed.len) { off_m = std.fmt.parseInt(i64, trimmed[idx .. idx + 2], 10) catch return null; idx += 2; } offset_sec = sign * (off_h * 3600 + off_m * 60); } else { return null; } } else { return null; } } if (idx != trimmed.len) return null; // trailing unparsed garbage
const days = daysFromCivil(year, month, day); const total_sec = days * 86400 + hour * 3600 + minute * 60 + second - offset_sec; return total_sec * 1000 + millis;}
test "parseIso8601 with various formats" { const cases = [_]struct { s: []const u8, expected: i64 }{ .{ .s = "2026-09-16T08:00:00.037593+00:00", .expected = 1789545600037 }, .{ .s = "2026-09-15T23:46:00Z", .expected = 1789515960000 }, .{ .s = "2026-06-09T11:28:39.481128Z", .expected = 1781004519481 }, .{ .s = "2026-01-01T00:00:00+05:30", .expected = 1767205800000 }, .{ .s = "2026-01-01T00:00:00-08:00", .expected = 1767254400000 }, .{ .s = "2026-01-01T00:00:00.5Z", .expected = 1767225600500 }, .{ .s = "2026-01-01T00:00:00.12Z", .expected = 1767225600120 }, .{ .s = "2026-01-01T00:00:00.123Z", .expected = 1767225600123 }, .{ .s = "2026-01-01T00:00:00.1234Z", .expected = 1767225600123 }, };
for (cases) |c| { const got = parseIso8601(c.s); try std.testing.expectEqual(c.expected, got.?); }
try std.testing.expect(parseIso8601("") == null); try std.testing.expect(parseIso8601("not-a-date") == null); try std.testing.expect(parseIso8601("2026-09-16") == null);}12123456789101189101112131414 unmodified lines29303132343533343536373839404142434445461 unmodified line484950515152535455565253545556616257585960616263646465666768697071727371727374757677787980818283757677787980818284858687888990917 unmodified lines991001011029899100101102103104105106107108109110111103104//! Small CLI over the core: for debugging, and for parity tests against the TypeScript//! reference. It reads one file it is handed and nothing else.//! CLI over the core: for debugging, and for parity tests against the TypeScript//! reference.const std = @import("std");const usage = @import("usage.zig");
const help_text = \\usage <command> \\ providers providers with a usage feed \\ parse <file> [--now-ms N] parse a provider body into limits \\ claims <access-token> account id and email from an OAuth token \\ request <provider> <token> [account-id] \\ providers supported providers info \\ advance <provider> <credential.json> <responses.json> [--now-ms N] advance feed state \\ claims <access-token> account id and email from an OAuth token \\;
fn writeTo(file: std.Io.File, io: std.Io, bytes: []const u8) !void {14 unmodified lines try writeTo(std.Io.File.stderr(), init.io, help_text); return 1;}
/// The host always passes `now` through the ABI; this is only so a person can run the/// CLI without knowing the epoch milliseconds.fn nowMs() i64 { var ts: std.posix.timespec = undefined; if (std.posix.errno(std.posix.system.clock_gettime(.REALTIME, &ts)) != .SUCCESS) return 0; return @as(i64, ts.sec) * 1000 + @divTrunc(@as(i64, ts.nsec), 1_000_000);}
fn readFile(init: std.process.Init, path: []const u8, arena: std.mem.Allocator) ![]u8 { return std.Io.Dir.cwd().readFileAlloc(init.io, path, arena, std.Io.Limit.limited(8 * 1024 * 1024));}
pub fn main(init: std.process.Init) !u8 { const arena = init.arena.allocator(); const argv = try std.process.Args.toSlice(init.minimal.args, arena); if (argv.len < 2) return fail(init, "missing command");1 unmodified line const command = argv[1];
if (std.mem.eql(u8, command, "providers")) { var w = usage.Writer.init(arena); try w.raw("["); for (usage.supported_providers, 0..) |provider, i| { if (i > 0) try w.raw(","); try w.string(provider); } try w.raw("]"); try usage.writeProviders(&w); try emit(init, w.items()); return 0; }
if (std.mem.eql(u8, command, "parse")) { if (argv.len < 3) return fail(init, "parse needs a file"); if (std.mem.eql(u8, command, "advance")) { if (argv.len < 5) return fail(init, "advance needs <provider> <credential.json> <responses.json>"); const provider = argv[2]; const cred_arg = argv[3]; const resp_arg = argv[4];
var now_ms = nowMs(); var i: usize = 3; var i: usize = 5; while (i < argv.len) : (i += 1) { if (std.mem.eql(u8, argv[i], "--now-ms") and i + 1 < argv.len) { now_ms = std.fmt.parseInt(i64, argv[i + 1], 10) catch now_ms; i += 1; } } const body = std.Io.Dir.cwd().readFileAlloc(init.io, argv[2], arena, std.Io.Limit.limited(8 * 1024 * 1024)) catch |e| { return fail(init, @errorName(e)); };
// Arguments could be raw JSON or file paths const cred_str = if (std.mem.startsWith(u8, cred_arg, "{")) cred_arg else readFile(init, cred_arg, arena) catch |e| return fail(init, @errorName(e));
const resp_str = if (std.mem.startsWith(u8, resp_arg, "[") or std.mem.startsWith(u8, resp_arg, "{")) resp_arg else readFile(init, resp_arg, arena) catch |e| return fail(init, @errorName(e));
var w = usage.Writer.init(arena); const outcome = try usage.parse(arena, "openai-codex", body, now_ms, &w); switch (outcome) { .ok => { try emit(init, w.items()); return 0; }, .err => |text| return fail(init, text), } const outcome = try usage.advance(arena, provider, cred_str, resp_str, now_ms); try usage.writeStep(&w, outcome); try emit(init, w.items()); return 0; }
if (std.mem.eql(u8, command, "claims")) { if (argv.len < 3) return fail(init, "claims needs an access token");7 unmodified lines .err => |text| return fail(init, text), } }
if (std.mem.eql(u8, command, "request")) { if (argv.len < 4) return fail(init, "request needs a provider and an access token"); const account: ?[]const u8 = if (argv.len > 4) argv[4] else null; var w = usage.Writer.init(arena); const outcome = try usage.buildRequest(argv[2], argv[3], account, &w); switch (outcome) { .ok => { try emit(init, w.items()); return 0; }, .err => |text| return fail(init, text), } }
return fail(init, "unknown command");}112345678934567128914151610111213141518192016171819202122232425262728293031323334353637383940414243444546474849505152535455565758232459606162636465666732333435363738394041424344454647484950515268697071727374545556757677787980818283848586878889909192939495969798991001011021031041051061071081099 unmodified lines1191201211227475761231241251265 unmodified lines13213313413513613713813914014114214314414514614714814915015115215315415515690157158159160161162163164981651661671681691701711721061071081731741751761772 unmodified lines1801811821831191201211221231241251261271841851861871881891901911301311321331921931941951961971981411421431441451992002012022032042052062072082092102112122132141471481491501511522152162172182192202212222232242251541551561571582262272282292302312322332342352362371612382392402412422432442452462472482492502512522532542552562572582592602612622632642652661641651661672672682692702712722732742752762772782792802812822831701711721731741751762842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373381781793393403413423433443453463473483493503513523533543553563573583593603613623633643653661813673683693703713723733743753763773783791831841851861871881891903803813823833843853863873883893903913923931921931941951961971981992003943952022032042052062072082092102112122132142152162172182192202212222232242253963973983994004014024034044054064074084094104114124132294144154164174184194204214224232312322332342352364244254264274284294304314324332382392402412422432442452462472482492502512522532542552564344354362582594374384394404414424434444454464474484494504514524534544554564572622632642652662672684584592702712724604614624634644 unmodified lines46947047147228528628728847347447547647747815 unmodified lines4944954964973124984995005015024 unmodified lines507508509510325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370//! Provider usage query core.//! Pure provider usage query core.//!//! Pure by construction: no sockets, no clock, no filesystem. The host resolves//! credentials, performs the request, and passes the body here, which is what lets a//! macOS app use NSURLSession instead of us shipping a TLS stack.//!//! The shapes match the JSON contract in packages/ai/docs/usage-query.md; the//! TypeScript implementation in packages/ai/src/utils/oauth/openai-codex.ts is the//! reference, and tests compare the two on the same bodies.//! Driven by the host: no sockets, no clock, no filesystem.//! Implements ABI 2 (host-driven requests/report protocol).const std = @import("std");pub const iso8601 = @import("iso8601.zig");
pub const abi_version: u32 = 1;pub const abi_version: u32 = 2;
/// Providers with a feed, in stable order. The host uses this to name providers that/// have no feed rather than implying none exist.pub const supported_providers = [_][]const u8{"openai-codex"};pub const ProviderInfo = struct { provider: []const u8, providerName: []const u8, credentialKinds: []const []const u8,};
pub const Outcome = union(enum) { ok, err: []const u8,pub const supported_providers = [_]ProviderInfo{ .{ .provider = "alibaba", .providerName = "Alibaba Model Studio", .credentialKinds = &[_][]const u8{"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{"apiKey"}, }, .{ .provider = "zai", .providerName = "Z.ai", .credentialKinds = &[_][]const u8{"apiKey"}, },};
/// One quota window, mirroring the `UsageLimit` shape.pub const Limit = struct {pub const UsageLimit = struct { label: []const u8, usedPercent: ?f64 = null, windowLabel: ?[]const u8 = null, resetsAt: ?i64 = null, status: []const u8, detail: ?[]const u8 = null,};
/// Write one JSON string literal, escaping what JSON requires.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 const UsageRequest = struct { method: []const u8 = "GET", url: []const u8, headers: []const [2][]const u8 = &[_][2][]const u8{}, body: ?[]const u8 = null,};
/// Append-only JSON writer over an arena. Deliberately hand-rolled: the shapes here are/// a handful of fields, and a std.json stringify API would put a moving target between/// the core and the contract.pub const UsageResponse = struct { status: i64, body: []const u8,};
pub const UsageCredential = struct { kind: []const u8, access: ?[]const u8 = null, apiKey: ?[]const u8 = null, accountId: ?[]const u8 = null, email: ?[]const u8 = null,};
pub const Outcome = union(enum) { requests: []const UsageRequest, report: ReportOutcome, err: []const u8,};
pub const ReportOutcome = struct { providerName: ?[]const u8 = null, 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,
9 unmodified lines try self.buf.append(self.alloc, c); }
pub fn print(self: *Writer, comptime fmt: []const u8, args: anytype) !void { // Formatting into a temporary and appending keeps this independent of the // std writer interface, which moves between Zig releases. Every call site has // an arena behind it, so the temporary costs nothing that outlives the request. const text = try std.fmt.allocPrint(self.alloc, fmt, args); try self.buf.appendSlice(self.alloc, text); }
5 unmodified lines 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('"');}
fn asObject(value: ?std.json.Value) ?std.json.ObjectMap {pub fn asObject(value: ?std.json.Value) ?std.json.ObjectMap { const v = value orelse return null; return switch (v) { .object => |o| o, else => null, };}
fn asArray(value: ?std.json.Value) ?std.json.Array {pub fn asArray(value: ?std.json.Value) ?std.json.Array { const v = value orelse return null; return switch (v) { .array => |a| a, else => null, };}
/// Numbers arrive as integers, floats, or numeric strings; anything else is absent./// Matches `asNumber` in the TypeScript reference.fn asNumber(value: ?std.json.Value) ?f64 {pub fn asNumber(value: ?std.json.Value) ?f64 { const v = value orelse return null; return switch (v) { .integer => |i| @floatFromInt(i), .float => |f| f,2 unmodified lines else => null, };}
/// "5h window" / "2d window"; absent when the window is unknown or non-positive.pub fn formatWindowLabel(alloc: std.mem.Allocator, seconds: ?f64) !?[]const u8 { const s = seconds orelse return null; if (!(s > 0)) return null; if (s >= 86_400) { return try std.fmt.allocPrint(alloc, "{d}d window", .{roundToInt(s / 86_400)}); } const hours = roundToInt(s / 3_600); return try std.fmt.allocPrint(alloc, "{d}h window", .{if (hours < 1) 1 else hours});pub fn asString(value: ?std.json.Value) ?[]const u8 { const v = value orelse return null; return switch (v) { .string => |s| s, else => null, };}
fn roundToInt(value: f64) i64 { return @intFromFloat(@round(value));}
pub fn statusFor(used_percent: ?f64) []const u8 { const used = used_percent orelse return "unknown"; if (used >= 100) return "exhausted"; if (used >= 90) return "warning"; return "ok";}
const Window = struct { used_percent: ?f64 = null, window: ?[]const u8 = null, resets_at: ?i64 = null,};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;}
/// `reset_at` above 1e12 is already milliseconds; below it is seconds. Otherwise/// `reset_after_seconds` counts from now.fn parseWindow(alloc: std.mem.Allocator, value: ?std.json.Value, now_ms: i64) !?Window { const obj = asObject(value) orelse return null; const used_percent = asNumber(obj.get("used_percent")); const window = try formatWindowLabel(alloc, asNumber(obj.get("limit_window_seconds")));pub fn formatWindowLabel(alloc: std.mem.Allocator, seconds: ?f64) !?[]const u8 { const s = seconds orelse return null; if (!(s > 0)) return null; if (s >= 86_400) { const days = @as(i64, @intFromFloat(@round(s / 86_400))); return try std.fmt.allocPrint(alloc, "{d}d window", .{days}); } const hours = @as(i64, @intFromFloat(@round(s / 3_600))); return try std.fmt.allocPrint(alloc, "{d}h window", .{if (hours < 1) 1 else hours});}
var resets_at: ?i64 = null; if (asNumber(obj.get("reset_at"))) |reset_at| { resets_at = if (reset_at > 1_000_000_000_000) @intFromFloat(reset_at) else @intFromFloat(reset_at * 1000); } else if (asNumber(obj.get("reset_after_seconds"))) |after| { resets_at = now_ms + @as(i64, @intFromFloat(after * 1000));pub fn epochToWindowLabel(alloc: std.mem.Allocator, resets_at_ms: ?i64, now_ms: i64) !?[]const u8 { const ra = resets_at_ms orelse return null; const diff = ra - now_ms; const seconds = @max(1, @as(i64, @intFromFloat(@round(@as(f64, @floatFromInt(diff)) / 1000.0)))); if (seconds >= 86_400) { const days = @as(i64, @intFromFloat(@round(@as(f64, @floatFromInt(seconds)) / 86_400.0))); return try std.fmt.allocPrint(alloc, "{d}d window", .{days}); } const hours = @max(1, @as(i64, @intFromFloat(@round(@as(f64, @floatFromInt(seconds)) / 3600.0)))); return try std.fmt.allocPrint(alloc, "{d}h window", .{hours});}
return .{ .used_percent = used_percent, .window = window, .resets_at = resets_at };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 = if (status_num) |s| @as(i64, @intFromFloat(s)) else 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});}
/// Dispatch to the provider's parser. Adding a provider means a parser plus a line here.pub fn parse(alloc: std.mem.Allocator, provider: []const u8, body: []const u8, now_ms: i64, w: *Writer) !Outcome { if (std.mem.eql(u8, provider, "openai-codex")) return parseCodex(alloc, body, now_ms, w); return .{ .err = "provider has no usage feed" };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;}
/// ChatGPT backend `wham/usage` body -> plan type and labelled windows.pub fn parseCodex(alloc: std.mem.Allocator, body: []const u8, now_ms: i64, w: *Writer) !Outcome { if (body.len == 0) return .{ .err = "usage body is empty" }; var parsed = std.json.parseFromSlice(std.json.Value, alloc, body, .{}) catch { return .{ .err = "invalid JSON body" }; }; defer parsed.deinit();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(','); try w.raw("{\"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('}'); } try w.raw("]}"); }, .report => |rep| { try w.raw("{\"report\":{"); var wrote = false; if (rep.providerName) |pn| { try w.raw("\"providerName\":"); try w.string(pn); wrote = true; } if (rep.accountId) |aid| { if (wrote) try w.byte(','); 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; }
const root = asObject(parsed.value) orelse return .{ .err = "usage body is not an object" }; const rate_limit = asObject(root.get("rate_limit")); if (wrote) try w.byte(','); try w.raw("\"limits\":["); for (rep.limits, 0..) |lim, i| { if (i > 0) try w.byte(','); try w.raw("{\"label\":"); try w.string(lim.label); if (lim.usedPercent) |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.resetsAt) |ra| { try w.print(",\"resetsAt\":{d}", .{ra}); } 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;
var limits: std.ArrayList(Limit) = .empty; 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(']'); }
if (try parseWindow(alloc, if (rate_limit) |rl| rl.get("primary_window") else null, now_ms)) |window| { try limits.append(alloc, .{ .label = "Primary", .usedPercent = window.used_percent, .windowLabel = window.window, .resetsAt = window.resets_at, .status = statusFor(window.used_percent), }); 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("}"); }, } if (try parseWindow(alloc, if (rate_limit) |rl| rl.get("secondary_window") else null, now_ms)) |window| { try limits.append(alloc, .{ .label = "Secondary", .usedPercent = window.used_percent, .windowLabel = window.window, .resetsAt = window.resets_at, .status = statusFor(window.used_percent), }); }}
if (asArray(root.get("additional_rate_limits"))) |additional| { for (additional.items) |entry| { const item = asObject(entry) orelse continue; const extra = asObject(item.get("rate_limit")); const name = stringField(item.get("limit_name")) orelse stringField(item.get("metered_feature")) orelse "Additional"; if (try parseWindow(alloc, if (extra) |e| e.get("primary_window") else null, now_ms)) |window| { try limits.append(alloc, .{ .label = try std.fmt.allocPrint(alloc, "{s} · Primary", .{name}), .usedPercent = window.used_percent, .windowLabel = window.window, .resetsAt = window.resets_at, .status = statusFor(window.used_percent), }); } if (try parseWindow(alloc, if (extra) |e| e.get("secondary_window") else null, now_ms)) |window| { try limits.append(alloc, .{ .label = try std.fmt.allocPrint(alloc, "{s} · Secondary", .{name}), .usedPercent = window.used_percent, .windowLabel = window.window, .resetsAt = window.resets_at, .status = statusFor(window.used_percent), }); }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("]");}
const plan_type = stringField(root.get("plan_type"));// Re-export providerspub 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");
try w.raw("{"); var wrote = false; if (plan_type) |plan| { try w.raw("\"planType\":"); try w.string(plan); wrote = true;pub fn advance( alloc: std.mem.Allocator, provider: []const u8, credential_json: []const u8, responses_json: []const u8, now_ms: i64,) !Outcome { if (std.mem.eql(u8, provider, "openai-codex")) { return codex.advance(alloc, credential_json, responses_json, now_ms); } if (wrote) try w.byte(','); try w.raw("\"limits\":["); for (limits.items, 0..) |limit, i| { if (i > 0) try w.byte(','); try w.raw("{\"label\":"); try w.string(limit.label); if (limit.usedPercent) |used| { try w.print(",\"usedPercent\":{d}", .{used}); } if (limit.windowLabel) |window_label| { try w.raw(",\"windowLabel\":"); try w.string(window_label); } if (limit.resetsAt) |resets_at| { try w.print(",\"resetsAt\":{d}", .{resets_at}); } try w.raw(",\"status\":"); try w.string(limit.status); try w.byte('}'); if (std.mem.eql(u8, provider, "anthropic")) { return anthropic.advance(alloc, credential_json, responses_json, now_ms); } try w.raw("]}"); return .ok; if (std.mem.eql(u8, provider, "google-antigravity")) { return antigravity.advance(alloc, credential_json, responses_json, now_ms); } if (std.mem.eql(u8, provider, "hyper")) { return hyper.advance(alloc, credential_json, responses_json, now_ms); } if (std.mem.eql(u8, provider, "opencode-go")) { return opencode_go.advance(alloc, credential_json, responses_json, now_ms); } if (std.mem.eql(u8, provider, "zai")) { return zai.advance(alloc, credential_json, responses_json, now_ms); } if (std.mem.eql(u8, provider, "deepseek")) { return deepseek.advance(alloc, credential_json, responses_json, now_ms); } if (std.mem.eql(u8, provider, "alibaba")) { return alibaba.advance(alloc, credential_json, responses_json, now_ms); } return .{ .err = "unknown provider" };}
fn stringField(value: ?std.json.Value) ?[]const u8 { const v = value orelse return null; return switch (v) { .string => |s| s, else => null, };}pub const ClaimsOutcome = union(enum) { ok, err: []const u8 };
/// OpenAI OAuth access token -> account id and email. The JWT payload is base64url and/// may be unpadded; claims live under provider-namespaced keys.pub fn claimsFromAccessToken(alloc: std.mem.Allocator, jwt: []const u8, w: *Writer) !Outcome {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 {4 unmodified lines 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| stringField(a.get("chatgpt_account_id")) else null; // The reference lowercases the profile email and drops a blank one, so hosts that // key accounts by claim email agree with the CLI. const email_claim = if (profile) |p| stringField(p.get("email")) else null; 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);15 unmodified lines try w.byte('}'); return .ok;}
fn jwtPayload(alloc: std.mem.Allocator, jwt: []const u8) ![]u8 {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;4 unmodified lines errdefer alloc.free(decoded); try decoder.decode(decoded, encoded); return decoded;}
/// What the host must send: URL plus headers, so a native host can use its own HTTP/// stack. Secrets stay with the caller; this only shapes them.pub fn buildRequest( provider: []const u8, access_token: []const u8, account_id: ?[]const u8, w: *Writer,) !Outcome { if (!std.mem.eql(u8, provider, "openai-codex")) { return .{ .err = "provider has no usage feed" }; } try w.raw("{\"url\":\"https://chatgpt.com/backend-api/wham/usage\",\"headers\":[[\"User-Agent\",\"prime-agent\"],[\"Authorization\",\"Bearer "); try writeJsonStringInline(w, access_token); try w.raw("\"]"); if (account_id) |id| { if (id.len > 0) { try w.raw(",[\"ChatGPT-Account-Id\",\""); try writeJsonStringInline(w, id); try w.raw("\"]"); } } try w.raw("]}"); return .ok;}
/// Escape a string's body without the surrounding quotes, for the two places above/// where a value is embedded mid-literal.fn writeJsonStringInline(w: *Writer, value: []const u8) !void { 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); } }, } }}