feat(native): implement the usage query core in Zig for native hosts
dawn wants the usage query from a macOS app, and JavaScript will not run there. This is the query alone — the rules that turn a provider body into limits — as a C ABI library in Zig, with the TypeScript implementation left in place as the reference. The core is pure on purpose: it never opens a socket, reads a clock, or touches the filesystem. - `usage_parse(provider, body, now_ms)`, `usage_build_request(provider, token, account_id)`, `usage_credentials_from_access_token(jwt)`, `usage_supported_providers()`, `usage_abi_version()`, `usage_free()`. JSON in, JSON out, so the shapes are the contract already documented in packages/ai/docs/usage-query.md. Failures come back as `{"error":"..."}` and NULL only ever means allocation failed; a host with its own token needs no auth store. - Host owns the socket. A usage query is one GET, and doing it here would mean shipping TLS with its own trust store and proxy behaviour; a host with NSURLSession already has that, so it asks for the request, sends it, and hands back the body. - `native/usage/tests/c_smoke.c` is compiled by the toolchain and linked against the installed static library, so `zig build test` exercises the ABI the way a native caller does — including a NULL argument and a provider without a feed. - `packages/ai/test/usage-zig-parity.test.ts` computes both implementations from the same recorded bodies at test time (one captured live, redacted of user_id/account_id/email), so neither can drift from the other quietly. - Zig is pinned to 0.16.0 with a verified checksum: 0.16 moved args, stdio, fs, and clock APIs, and the pure core compiled unchanged through that while only the debug CLI needed edits, which is the boundary doing its job. `npm run check` now includes `check:zig`, and CI installs the pinned toolchain. Verified: zig unit tests plus the C ABI smoke test, byte-identical output against the TypeScript reference on both fixtures, `npm run check` clean.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576# usage (Zig)
The usage query core, for hosts that cannot run the TypeScript implementation. This isthe same query `prime-agent usage --json` performs, as a C ABI library that a macOS appcan call in-process.
`packages/ai` keeps the TypeScript implementation. It stays the reference: it generatedthe fixtures here, the parity test compares the two on the same bodies, and the JSONshapes are the contract in `packages/ai/docs/usage-query.md`.
## The boundary: the core is pure, the host owns I/O
The library never opens a socket, reads a clock, or touches the filesystem. It exports:
| function | purpose || --- | --- || `usage_abi_version()` | ABI version, bumped on any signature or shape change. || `usage_supported_providers()` | JSON array of provider ids with a feed. || `usage_credentials_from_access_token(jwt, len)` | `{"accountId"?, "email"?}` from OAuth token claims. || `usage_build_request(provider, len, token, token_len, account_id, account_id_len)` | `{"url", "headers": [[name, value], …]}` for the host to send. || `usage_parse(provider, len, body, body_len, now_ms)` | `{"planType"?, "limits": [...]}`. || `usage_free(ptr)` | Release a returned string. NULL is accepted. |
Every call returns a NUL-terminated JSON string the caller frees with `usage_free`, andreports failure **inside** that JSON (`{"error":"..."}`). NULL means only that allocationfailed. Optional strings may be NULL with length 0.
That split is deliberate. A usage query is one GET, and doing it here would mean shippinga TLS stack with its own trust store, proxy handling, and HTTP version behaviour. A hostthat already has NSURLSession gets all of that for free, so the app asks the core for therequest, performs it, and hands the body back for parsing.
## Build and test
Pinned toolchain: **Zig 0.16.0**. The standard library moved `args`, `stdio`, `fs`, and`clock` APIs in 0.16, so the version is not incidental.
```sh# hermetic install (checksum from ziglang.org/download/index.json)curl -L -o /tmp/zig.tar.xz https://ziglang.org/download/0.16.0/zig-aarch64-macos-0.16.0.tar.xzshasum -a 256 /tmp/zig.tar.xz # b23d70deaa879b5c2d486ed3316f7eaa53eacf6fc9cc747de152450d401489mkdir -p ~/.local/share/zig && tar -xJf /tmp/zig.tar.xz -C ~/.local/share/zigexport PATH="$HOME/.local/share/zig/zig-aarch64-macos-0.16.0:$PATH"
zig build # libusage.a, libusage.dylib, usage.h, usage CLIzig build test # core unit tests + the C ABI smoke testzig build smoke # C ABI smoke test alonezig build run -- providerszig build run -- parse tests/fixtures/codex-usage-live.json --now-ms 1789514000000zig build run -- claims <access-token>```
The `smoke` step is not decoration: it compiles `tests/c_smoke.c` with the toolchain,links the installed static library, and calls the ABI the way a native host does.
### Parity with the TypeScript reference
```shZIG=$(command -v zig) npm --prefix packages/ai run test test/usage-zig-parity.test.ts```
Both sides compute from the same fixture at test time, so a change to eitherimplementation fails the test rather than drifting quietly. The fixtures under`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.
## Status
- One provider has a feed: `openai-codex`. Others return `{"error":"provider has no usage feed"}`, which is how a host names providers that have none instead of implying they have no limits.- Bodies carry more than the parser reads today — `credits`, `spend_control`, `code_review_rate_limit` — so new limits are a parser change plus a fixture.- No Swift glue yet: the app-side wrapper (module map, `String` marshalling) comes with the first real caller.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283const std = @import("std");
pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{});
// The core is a module so tests import it by name instead of reaching across // directory boundaries. const core = b.createModule(.{ .root_source_file = b.path("src/usage.zig"), .target = target, .optimize = optimize, });
// Shared library for native hosts (Swift imports include/usage.h). const lib = b.addLibrary(.{ .name = "usage", .linkage = .dynamic, .root_module = b.createModule(.{ .root_source_file = b.path("src/c_api.zig"), .target = target, .optimize = optimize, .link_libc = true, }), }); lib.installHeader(b.path("include/usage.h"), "usage.h"); b.installArtifact(lib);
const static_lib = b.addLibrary(.{ .name = "usage", .linkage = .static, .root_module = b.createModule(.{ .root_source_file = b.path("src/c_api.zig"), .target = target, .optimize = optimize, .link_libc = true, }), }); b.installArtifact(static_lib);
const exe = b.addExecutable(.{ .name = "usage", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), }); b.installArtifact(exe);
const run = b.addRunArtifact(exe); if (b.args) |args| run.addArgs(args); b.step("run", "Run the usage CLI").dependOn(&run.step);
const test_module = b.createModule(.{ .root_source_file = b.path("tests/usage_test.zig"), .target = target, .optimize = optimize, }); test_module.addImport("usage", core); const tests = b.addTest(.{ .root_module = test_module }); const run_tests = b.addRunArtifact(tests);
// The ABI is the part native hosts consume, so it is exercised the way they call it: // a C program compiled with this toolchain, linked against the installed static // library, reading a recorded provider body. const smoke_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true, }); smoke_module.addCSourceFile(.{ .file = b.path("tests/c_smoke.c"), .flags = &.{ "-Wall", "-Wextra" } }); smoke_module.addIncludePath(b.path("include")); smoke_module.linkLibrary(static_lib); const smoke = b.addExecutable(.{ .name = "c_smoke", .root_module = smoke_module }); const run_smoke = b.addRunArtifact(smoke); run_smoke.addFileArg(b.path("tests/fixtures/codex-usage-live.json"));
b.step("smoke", "Compile and run the C ABI smoke test").dependOn(&run_smoke.step); const test_step = b.step("test", "Run core tests and the C ABI smoke test"); test_step.dependOn(&run_tests.step); test_step.dependOn(&run_smoke.step);}1234567891011121314.{ .name = .usage, .version = "0.1.0", .minimum_zig_version = "0.16.0", .fingerprint = 0xd0eb5e70879e9dba, .paths = .{ "build.zig", "build.zig.zon", "src", "include", "tests", "README.md", },}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748/* 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. * * 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". * Release results with usage_free. */#ifndef USAGE_H#define USAGE_H
#include <stddef.h>#include <stdint.h>
#ifdef __cplusplusextern "C" {#endif
/* ABI version. Bump on any change to the exported signatures or their shapes. */uint32_t usage_abi_version(void);
/* Release a string returned by this library. NULL is accepted. */void usage_free(char *ptr);
/* {"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
#endif /* USAGE_H */123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117//! C ABI over the pure core.//!//! One rule for every export: return a JSON string the caller frees with `usage_free`,//! and report failure *inside* that JSON. A host never has to tell "no error" apart from//! "no data", and NULL means only that allocation failed.const std = @import("std");const usage = @import("usage.zig");
const alloc = std.heap.c_allocator;
/// C callers (Swift included) pass NULL for an absent string; treat it as empty rather/// than dereferencing a null pointer.fn slice(ptr: ?[*]const u8, len: usize) []const u8 { const p = ptr orelse return ""; return p[0..len];}
fn finish(w: *usage.Writer) ?[*:0]u8 { return alloc.dupeZ(u8, w.items()) catch null;}
fn errorResult(a: std.mem.Allocator, text: []const u8) ?[*:0]u8 { var w = usage.Writer.init(a); w.raw("{\"error\":") catch return null; w.string(text) catch return null; w.raw("}") catch return null; return finish(&w);}
fn message(error_code: anyerror) []const u8 { return switch (error_code) { error.OutOfMemory => "out of memory", else => "usage query failed", };}
export fn usage_abi_version() u32 { return usage.abi_version;}
export fn usage_free(ptr: ?[*:0]u8) void { if (ptr) |p| alloc.free(std.mem.span(p));}
export fn usage_supported_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; return finish(&w);}
export fn usage_parse( provider: ?[*]const u8, provider_len: usize, body: ?[*]const u8, body_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| { 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| { return errorResult(a, message(e)); }; switch (outcome) { .ok => return finish(&w), .err => |text| return errorResult(a, text), }}
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 { 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| { return errorResult(a, message(e)); }; switch (outcome) { .ok => return finish(&w), .err => |text| return errorResult(a, text), }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113//! 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.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] \\;
fn writeTo(file: std.Io.File, io: std.Io, bytes: []const u8) !void { var buffer: [8192]u8 = undefined; var writer = file.writer(io, &buffer); try writer.interface.writeAll(bytes); try writer.flush();}
fn emit(init: std.process.Init, bytes: []const u8) !void { try writeTo(std.Io.File.stdout(), init.io, bytes); try writeTo(std.Io.File.stdout(), init.io, "\n");}
fn fail(init: std.process.Init, message: []const u8) !u8 { try writeTo(std.Io.File.stderr(), init.io, message); try writeTo(std.Io.File.stderr(), init.io, "\n\n"); 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);}
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");
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 emit(init, w.items()); return 0; }
if (std.mem.eql(u8, command, "parse")) { if (argv.len < 3) return fail(init, "parse needs a file"); var now_ms = nowMs(); var i: usize = 3; 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)); }; 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), } }
if (std.mem.eql(u8, command, "claims")) { if (argv.len < 3) return fail(init, "claims needs an access token"); var w = usage.Writer.init(arena); const outcome = try usage.claimsFromAccessToken(arena, argv[2], &w); switch (outcome) { .ok => { try emit(init, w.items()); return 0; }, .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");}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370//! 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.const std = @import("std");
pub const abi_version: u32 = 1;
/// 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 Outcome = union(enum) { ok, err: []const u8,};
/// One quota window, mirroring the `UsageLimit` shape.pub const Limit = struct { label: []const u8, usedPercent: ?f64 = null, windowLabel: ?[]const u8 = null, resetsAt: ?i64 = null, status: []const u8,};
/// 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('"');}
/// 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 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 { // 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); }
pub fn string(self: *Writer, value: []const u8) !void { try writeJsonString(self, value); }
pub fn items(self: *Writer) []const u8 { return self.buf.items; }};
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 { 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 { const v = value orelse return null; return switch (v) { .integer => |i| @floatFromInt(i), .float => |f| f, .number_string => |s| std.fmt.parseFloat(f64, s) catch null, .string => |s| std.fmt.parseFloat(f64, s) catch null, 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});}
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,};
/// `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")));
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)); }
return .{ .used_percent = used_percent, .window = window, .resets_at = resets_at };}
/// 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" };}
/// 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();
const root = asObject(parsed.value) orelse return .{ .err = "usage body is not an object" }; const rate_limit = asObject(root.get("rate_limit"));
var limits: std.ArrayList(Limit) = .empty;
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), }); } 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), }); } } }
const plan_type = stringField(root.get("plan_type"));
try w.raw("{"); var wrote = false; if (plan_type) |plan| { try w.raw("\"planType\":"); try w.string(plan); wrote = true; } 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('}'); } try w.raw("]}"); return .ok;}
fn stringField(value: ?std.json.Value) ?[]const u8 { const v = value orelse return null; return switch (v) { .string => |s| s, else => null, };}
/// 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 { 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| 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 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;}
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;}
/// 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); } }, } }}123456789101112131415161718192021222324252627282930313233343536373839404142434445#include <stdio.h>#include <stdlib.h>#include <string.h>#include "usage.h"
static char *read_file(const char *path) { FILE *f = fopen(path, "rb"); if (!f) return NULL; fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); char *buffer = malloc((size_t)size + 1); fread(buffer, 1, (size_t)size, f); buffer[size] = '\0'; fclose(f); return buffer;}
int main(int argc, char **argv) { if (argc < 2) return 2; printf("abi=%u supported=%s\n", usage_abi_version(), usage_supported_providers());
char *body = read_file(argv[1]); if (!body) { fprintf(stderr, "cannot read %s\n", argv[1]); return 2; }
char *report = usage_parse("openai-codex", strlen("openai-codex"), body, strlen(body), 1789514000000LL); printf("%s\n", report); usage_free(report);
char *unknown = usage_parse("anthropic", strlen("anthropic"), body, strlen(body), 0); printf("%s\n", unknown); usage_free(unknown);
char *request = usage_build_request("openai-codex", strlen("openai-codex"), "tok", 3, "acct", 4); printf("%s\n", request); usage_free(request);
char *none = usage_build_request("openai-codex", strlen("openai-codex"), NULL, 0, NULL, 0); printf("null-safe=%s\n", none == NULL ? "null" : "ok"); usage_free(none); usage_free(NULL);
free(body); return 0;}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139//! Tests for the pure core. Everything here runs offline: the core has no I/O to fake.const std = @import("std");const usage = @import("usage");
const jwt = "eyJhbGciOiJSUzI1NiJ9.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdF90ZXN0XzEifSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9wcm9maWxlIjp7ImVtYWlsIjoiVGVzdC5BY2NvdW50QEV4YW1wbGUuQ09NIn19.c2ln";
fn parse(alloc: std.mem.Allocator, body: []const u8, now_ms: i64) ![]const u8 { var w = usage.Writer.init(alloc); const outcome = try usage.parse(alloc, "openai-codex", body, now_ms, &w); if (outcome == .err) return error.ParseFailed; return w.items();}
test "window labels match the reference formatting" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator(); try std.testing.expectEqualStrings("5h window", (try usage.formatWindowLabel(a, 18_000)).?); try std.testing.expectEqualStrings("7d window", (try usage.formatWindowLabel(a, 604_800)).?); try std.testing.expectEqualStrings("1h window", (try usage.formatWindowLabel(a, 60)).?); try std.testing.expect((try usage.formatWindowLabel(a, 0)) == null); try std.testing.expect((try usage.formatWindowLabel(a, -60)) == null); try std.testing.expect((try usage.formatWindowLabel(a, null)) == null);}
test "status thresholds: exhausted at 100, warning at 90" { try std.testing.expectEqualStrings("exhausted", usage.statusFor(100)); try std.testing.expectEqualStrings("exhausted", usage.statusFor(120)); try std.testing.expectEqualStrings("warning", usage.statusFor(90)); try std.testing.expectEqualStrings("ok", usage.statusFor(89.9)); try std.testing.expectEqualStrings("unknown", usage.statusFor(null));}
test "codex body: primary, secondary, and additional limits" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const body = \\{"plan_type":"prolite", \\ "rate_limit":{"primary_window":{"used_percent":100,"limit_window_seconds":604800}, \\ "secondary_window":{"used_percent":32.5,"limit_window_seconds":18000,"reset_after_seconds":3600}}, \\ "additional_rate_limits":[{"limit_name":"Spark","rate_limit":{"primary_window":{"used_percent":0,"limit_window_seconds":18000}}}]} ; const now: i64 = 1_700_000_000_000; const text = try parse(a, body, now);
try std.testing.expect(std.mem.indexOf(u8, text, "\"planType\":\"prolite\"") != null); try std.testing.expect(std.mem.indexOf(u8, text, "\"label\":\"Primary\",\"usedPercent\":100,\"windowLabel\":\"7d window\",\"status\":\"exhausted\"") != null); try std.testing.expect(std.mem.indexOf(u8, text, "\"label\":\"Spark \u{b7} Primary\"") != null); // reset_after_seconds is relative to now try std.testing.expect(std.mem.indexOf(u8, text, "\"resetsAt\":1700003600000") != null); // unknown percentages omit usedPercent but keep a status try std.testing.expect(std.mem.indexOf(u8, text, "\"status\":\"unknown\"") == null);}
test "reset_at in seconds is scaled, milliseconds are kept" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const seconds = try parse(a, "{\"rate_limit\":{\"primary_window\":{\"used_percent\":1,\"reset_at\":1700000000}}}", 0); try std.testing.expect(std.mem.indexOf(u8, seconds, "\"resetsAt\":1700000000000") != null);
const millis = try parse(a, "{\"rate_limit\":{\"primary_window\":{\"used_percent\":1,\"reset_at\":1700000000000}}}", 0); try std.testing.expect(std.mem.indexOf(u8, millis, "\"resetsAt\":1700000000000") != null);}
test "numeric strings parse like numbers, junk is absent" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const text = try parse(a, "{\"rate_limit\":{\"primary_window\":{\"used_percent\":\"75\"}}}", 0); try std.testing.expect(std.mem.indexOf(u8, text, "\"usedPercent\":75,\"status\":\"ok\"") != null);
const junk = try parse(a, "{\"rate_limit\":{\"primary_window\":{\"used_percent\":\"lots\"}}}", 0); try std.testing.expect(std.mem.indexOf(u8, junk, "usedPercent") == null); try std.testing.expect(std.mem.indexOf(u8, junk, "\"status\":\"unknown\"") != null);}
test "unknown windows are skipped, missing rate_limit is empty" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const text = try parse(a, "{\"rate_limit\":{\"primary_window\":{\"used_percent\":5}}}", 0); try std.testing.expect(std.mem.indexOf(u8, text, "windowLabel") == null); try std.testing.expect(std.mem.indexOf(u8, text, "\"label\":\"Primary\"") != null);
const empty = try parse(a, "{}", 0); try std.testing.expectEqualStrings("{\"limits\":[]}", empty);}
test "bad bodies are errors, not empty reports" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
var w = usage.Writer.init(a); try std.testing.expect((try usage.parse(a, "openai-codex", "not json", 0, &w)) == .err);
var w2 = usage.Writer.init(a); try std.testing.expect((try usage.parse(a, "anthropic", "{}", 0, &w2)) == .err);}
test "access token claims give account id and a normalized email" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
var w = usage.Writer.init(a); const outcome = try usage.claimsFromAccessToken(a, jwt, &w); try std.testing.expect(outcome == .ok); try std.testing.expectEqualStrings("{\"accountId\":\"acct_test_1\",\"email\":\"test.account@example.com\"}", w.items());
var broken = usage.Writer.init(a); try std.testing.expect((try usage.claimsFromAccessToken(a, "not-a-jwt", &broken)) == .err);}
test "request shape carries the token and only sends an account header when known" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
var w = usage.Writer.init(a); try std.testing.expect((try usage.buildRequest("openai-codex", "tok", "acct", &w)) == .ok); try std.testing.expectEqualStrings( "{\"url\":\"https://chatgpt.com/backend-api/wham/usage\",\"headers\":[[\"User-Agent\",\"prime-agent\"],[\"Authorization\",\"Bearer tok\"],[\"ChatGPT-Account-Id\",\"acct\"]]}", w.items(), );
var without = usage.Writer.init(a); try std.testing.expect((try usage.buildRequest("openai-codex", "tok", null, &without)) == .ok); try std.testing.expect(std.mem.indexOf(u8, without.items(), "ChatGPT-Account-Id") == null);
var foreign = usage.Writer.init(a); try std.testing.expect((try usage.buildRequest("anthropic", "tok", null, &foreign)) == .err);}