fix(core): bump to ABI 3 with host-owned state, safe float casts, and pipe draining
19 unmodified lines20212223242425262726282930313220 unmodified lines5354555655575859606160626364656640 unmodified lines1071081091101091101111111121131141151 unmodified line19 unmodified lines
| function | purpose || --- | --- || `usage_abi_version()` | ABI version, bumped on any signature or shape change. || `usage_supported_providers()` | JSON array of provider ids, names and credential kinds. || `usage_providers()` | JSON array of provider ids, names and credential kinds. || `usage_state_new()` | Allocate opaque cross-round state for multi-round feeds (e.g. Hyper). || `usage_state_free(state)` | Free opaque cross-round state. NULL is accepted. || `usage_credentials_from_access_token(jwt, len)` | `{"accountId"?, "email"?}` from OAuth token claims. || `usage_advance(provider, len, credential_json, credential_len, responses_json, responses_len, now_ms)` | Requests to answer, or the finished report. || `usage_advance(state, provider, len, credential_json, credential_len, responses_json, responses_len, now_ms)` | Requests to answer, or the finished report. || `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 allocation20 unmodified lineszig 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 -- advance <provider> <credential.json> <responses.json> [--now-ms N]zig build run -- advance <provider> <credential.json> <responses.json>... [--now-ms N]zig build run -- claims <access-token>```
The `smoke` step is not decoration: it compiles `tests/c_smoke.c` with the toolchain, linksthe installed static library, and calls the ABI the way a native host does.the installed static library, exercises multi-round state, and asserts against recorded provider bodies.
```shswift build --package-path swiftswift run --package-path swift usage-smoke tests/fixtures40 unmodified lines
## Status
- Eight providers have feeds. Unknown providers return `{"error":"unknown provider"}` and known providers with no feed return `{"error":"provider has no usage feed"}`, which is how a host names the gap instead of implying a provider has no limits. `{"error":"unknown provider"}`.- Providers differ in what they can answer from. Alibaba's token-plan keys are inference-only, so its feed prefers the official `bl` CLI and falls back to a captured console session; a provider whose answer needs a second round trips the host loop again.- Bodies carry more than the parsers read today, so new limits are a parser change plus a1 unmodified line34 unmodified lines3536373839394041424344454647484950515217 unmodified lines7071727366747576777813 unmodified lines34 unmodified lines .optimize = optimize, .link_libc = true, }), }); b.installArtifact(static_lib); const install_static = b.addInstallArtifact(static_lib, .{}); b.getInstallStep().dependOn(&install_static.step);
if (target.result.os.tag == .macos) { const ranlib_cmd = b.addSystemCommand(&[_][]const u8{"ranlib"}); ranlib_cmd.addFileArg(b.getInstallStep().owner.path("zig-out/lib/libusage.a")); ranlib_cmd.step.dependOn(&install_static.step); b.getInstallStep().dependOn(&ranlib_cmd.step); }
const exe = b.addExecutable(.{ .name = "usage", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"),17 unmodified lines 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. // library, exercising multi-round state and recorded provider bodies. const smoke_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true,13 unmodified lines17 unmodified lines1819202122222324252627282930313233343536373839404142434445374647394849505152538 unmodified lines17 unmodified lines#ifdef __cplusplusextern "C" {#endif
/* ABI version. Bumped to 2 for host-driven requests/report protocol. *//* ABI version. Bumped to 3 for explicit host-owned cross-call state. */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);
/* Opaque state owned by the host across multi-round feeds (e.g. Hyper). * The core owns no cross-call state; the host owns it and passes it back. * Pass NULL for single-round or stateless feeds. */typedef struct usage_state usage_state;
usage_state *usage_state_new(void);void usage_state_free(usage_state *state);
/* Advance feed state. * Returns either: * {"requests":[{"kind":"http","method","url","headers":[["k","v"]],"body"?}]} * or: * {"requests":[{"kind":"command","command","args":[],"stdin"?,"timeoutMs"?}]} * or: * {"report":{providerName, accountId?, email?, planType?, limits[], balance?, notes?[], error?}} * {"report":{accountId?, email?, planType?, limits[], balance?, notes?[], error?}} */char *usage_advance(const char *provider, size_t provider_len,char *usage_advance(usage_state *state, 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);
8 unmodified lines1 unmodified line2345678910111213141516171819202126 unmodified lines4849505152535455565758596061626364656667683 unmodified lines7273747576777879807 unmodified lines88899091929372949596979820 unmodified lines1 unmodified line//!//! 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.//!//! The core owns no cross-call state; the host owns it via usage_state and passes it back.const std = @import("std");const usage = @import("usage.zig");
const alloc = std.heap.c_allocator;
/// Opaque cross-round state struct owned by the host.pub const usage_state = struct { inner: usage.State,};
/// 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 "";26 unmodified linesexport fn usage_free(ptr: ?[*:0]u8) void { if (ptr) |p| alloc.free(std.mem.span(p));}
export fn usage_state_new() ?*usage_state { const s = alloc.create(usage_state) catch return null; s.inner = usage.State.init(alloc); return s;}
export fn usage_state_free(state: ?*usage_state) void { if (state) |s| { s.inner.deinit(); alloc.destroy(s); }}
export fn usage_providers() ?[*:0]u8 { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); const a = arena.allocator();3 unmodified lines return finish(&w);}
export fn usage_advance( state: ?*usage_state, provider: ?[*]const u8, provider_len: usize, credential_json: ?[*]const u8, credential_len: usize,7 unmodified lines
const prov_slice = slice(provider, provider_len); const cred_slice = slice(credential_json, credential_len); const resp_slice = slice(responses_json, responses_len); const state_ptr: ?*usage.State = if (state) |s| &s.inner else null;
const outcome = usage.advance(a, prov_slice, cred_slice, resp_slice, now_ms) catch |e| { const outcome = usage.advance(a, state_ptr, prov_slice, cred_slice, resp_slice, now_ms) catch |e| { return errorResult(a, message(e)); };
var w = usage.Writer.init(a);20 unmodified lines3 unmodified lines456789108910111213141539 unmodified lines55565758585960616162636464656667686970717273747275767778798081787980818283848384858685868788899091929394959697989910010110210310414 unmodified lines3 unmodified linesconst usage = @import("usage.zig");
const help_text = \\usage <command> \\ 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 \\ providers supported providers info \\ advance <provider> <credential.json> <responses.json>... advance feed state across rounds \\ [--now-ms N] \\ claims <access-token> account id and email from an OAuth token \\;
fn writeTo(file: std.Io.File, io: std.Io, bytes: []const u8) !void {39 unmodified lines return 0; }
if (std.mem.eql(u8, command, "advance")) { if (argv.len < 5) return fail(init, "advance needs <provider> <credential.json> <responses.json>"); 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 resp_args = std.ArrayList([]const u8).empty; var now_ms = nowMs(); var i: usize = 5; var i: usize = 4; 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; } else { try resp_args.append(arena, argv[i]); } }
// Arguments could be raw JSON or file paths if (resp_args.items.len == 0) return fail(init, "advance needs at least one responses argument");
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 state = usage.State.init(arena); defer state.deinit();
var w = usage.Writer.init(arena); const outcome = try usage.advance(arena, provider, cred_str, resp_str, now_ms); try usage.writeStep(&w, outcome); try emit(init, w.items()); for (resp_args.items) |resp_arg| { 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 = usage.advance(arena, &state, provider, cred_str, resp_str, now_ms) catch |e| { var err_w = usage.Writer.init(arena); usage.writeStep(&err_w, .{ .err = @errorName(e) }) catch return 1; try emit(init, err_w.items()); return 0; }; try usage.writeStep(&w, outcome); try emit(init, w.items()); } return 0; }
if (std.mem.eql(u8, command, "claims")) {14 unmodified lines123445678891011121342 unmodified lines5657585960616263646566676869707172737475767778798081828384858687888920 unmodified lines1101111121138788899091929394959697989911411511611711811912010712112212312436 unmodified lines16116216316415216516616716816926 unmodified lines1961971981991872002011892022032042052062072082092102112122132142 unmodified lines21721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824915 unmodified lines26526626726822926927023127127227323427427527627727827928024128128228328424328528628724628828929029129229329425329529629729829917 unmodified lines31731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836962 unmodified lines43243343443534935035135235343635543743843944016 unmodified lines45745845946046138038146246346446546646746868 unmodified lines53753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357446546657557657757846846947047147247347447547647747847948048148248348448548648748857958058158252 unmodified lines//! Pure provider usage query core.//!//! Driven by the host: no sockets, no clock, no filesystem.//! Implements ABI 2 (host-driven requests/report protocol).//! Implements ABI 3 (explicit host-owned cross-call state).//! 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 = 2;pub const abi_version: u32 = 3;
pub const ProviderInfo = struct { provider: []const u8, providerName: []const u8,42 unmodified lines .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 { label: []const u8, usedPercent: ?f64 = null, windowLabel: ?[]const u8 = null,20 unmodified lines stdin: ?[]const u8 = null, timeoutMs: ?i64 = null,};
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{},36 unmodified linespub fn writeJsonString(w: *Writer, value: []const u8) !void { try w.byte('"'); for (value) |c| { switch (c) { '"' => try w.raw("\\\""), '"' => try w.raw("\""), '\\' => try w.raw("\\\\"), '\n' => try w.raw("\\n"), '\r' => try w.raw("\\r"), '\t' => try w.raw("\\t"),26 unmodified lines}
pub fn asNumber(value: ?std.json.Value) ?f64 { const v = value orelse return null; return switch (v) { const f: ?f64 = switch (v) { .integer => |i| @floatFromInt(i), .float => |f| f, .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;2 unmodified lines 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";}15 unmodified lines}
pub fn formatWindowLabel(alloc: std.mem.Allocator, seconds: ?f64) !?[]const u8 { const s = seconds orelse return null; if (!(s > 0)) return null; if (!std.math.isFinite(s) or !(s > 0)) return null; if (s >= 86_400) { const days = @as(i64, @intFromFloat(@round(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 = @as(i64, @intFromFloat(@round(s / 3_600))); 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; const diff = ra - now_ms; const seconds = @max(1, @as(i64, @intFromFloat(@round(@as(f64, @floatFromInt(diff)) / 1000.0)))); if (diff <= 0) return null; 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 = @as(i64, @intFromFloat(@round(@as(f64, @floatFromInt(seconds)) / 86_400.0))); 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, @as(i64, @intFromFloat(@round(@as(f64, @floatFromInt(seconds)) / 3600.0)))); 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 = if (status_num) |s| @as(i64, @intFromFloat(s)) else return "HTTP no response"; 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| {17 unmodified lines } 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| {62 unmodified lines }, .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; }16 unmodified lines if (i > 0) try w.byte(','); 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}); 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);68 unmodified linespub 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, "openai-codex")) { return codex.advance(alloc, credential_json, responses_json, now_ms); if (std.mem.eql(u8, provider, entry.name)) { return entry.advance(alloc, state, credential_json, responses_json, now_ms); } } if (std.mem.eql(u8, provider, "anthropic")) { return anthropic.advance(alloc, credential_json, responses_json, now_ms); } 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" };}
pub const ClaimsOutcome = union(enum) { ok, err: []const u8 };52 unmodified lines1 unmodified line23456789101112131415165 unmodified lines22232425262728293031323334353637383940414243444546474849505152535455565758596061626364652266676869707126727374752930767778798081333435363738394041424344454682838485868788899091929349509495969798991001011021031041051061071081091101111121131141151161171181191201211221231245354125126127128129130131132581331341351 unmodified line#include <stdlib.h>#include <string.h>#include "usage.h"
static void check(int condition, const char *msg) { if (!condition) { fprintf(stderr, "FAIL: %s\n", msg); exit(1); }}
static char *read_file(const char *path) { FILE *f = fopen(path, "rb"); if (!f) return NULL; fseek(f, 0, SEEK_END);5 unmodified lines fclose(f); return buffer;}
static char *wrap_response_json(const char *body) { size_t body_len = strlen(body); size_t cap = body_len * 2 + 64; char *out = malloc(cap); strcpy(out, "[{\"status\":200,\"body\":\""); size_t pos = strlen(out); for (size_t i = 0; i < body_len; i++) { char c = body[i]; if (c == '"') { out[pos++] = '\\'; out[pos++] = '"'; } else if (c == '\\') { out[pos++] = '\\'; out[pos++] = '\\'; } else if (c == '\n') { out[pos++] = '\\'; out[pos++] = 'n'; } else if (c == '\r') { out[pos++] = '\\'; out[pos++] = 'r'; } else if (c == '\t') { out[pos++] = '\\'; out[pos++] = 't'; } else { out[pos++] = c; } } out[pos++] = '"'; out[pos++] = '}'; out[pos++] = ']'; out[pos] = '\0'; return out;}
int main(int argc, char **argv) { if (argc < 2) return 2;
check(usage_abi_version() == 3, "ABI version must be 3");
char *providers = usage_providers(); printf("abi=%u providers=%s\n", usage_abi_version(), providers); check(providers != NULL, "providers must not be NULL"); check(strstr(providers, "\"provider\":\"openai-codex\"") != NULL, "codex provider present"); check(strstr(providers, "\"provider\":\"hyper\"") != NULL, "hyper provider present"); usage_free(providers);
char *body = read_file(argv[1]); if (!body) { fprintf(stderr, "cannot read %s\n", argv[1]); return 2; } check(body != NULL, "cannot read fixture file");
/* 1. Codex step 1: asks for request */ const char *cred = "{\"kind\":\"oauth\",\"access\":\"tok\",\"email\":\"user@example.com\"}"; char *first_step = usage_advance("openai-codex", strlen("openai-codex"), cred, strlen(cred), "[]", 2, 0); printf("first_step=%s\n", first_step); char *first_step = usage_advance(NULL, "openai-codex", strlen("openai-codex"), cred, strlen(cred), "[]", 2, 0); check(first_step != NULL, "first step should not be NULL"); check(strstr(first_step, "\"url\":\"https://chatgpt.com/backend-api/wham/usage\"") != NULL, "codex url"); check(strstr(first_step, "\"kind\":\"http\"") != NULL, "codex request kind http"); usage_free(first_step);
/* Construct responses json: [{"status":200,"body": <escaped body>}] */ /* For simplicity in smoke test, build a small JSON wrapper around body */ size_t resp_len = strlen(body) + 64; char *resp = malloc(resp_len); /* Serialize body inside responses JSON string properly */ snprintf(resp, resp_len, "[{\"status\":200,\"body\":"); /* If body is already json, we can test advance with a simple response or pass raw */ /* But responses expects an array of {status, body: string} */ /* Let's construct a minimal valid responses_json */ free(resp);
const char *resp_json = "[{\"status\":200,\"body\":\"{\\\"plan_type\\\":\\\"plus\\\"}\"}]"; char *report = usage_advance("openai-codex", strlen("openai-codex"), cred, strlen(cred), resp_json, strlen(resp_json), 1789514000000LL); printf("report=%s\n", report); /* 2. Codex step 2: drives recorded body from argv[1] (codex-usage-live.json) */ char *resp_json = wrap_response_json(body); char *report = usage_advance(NULL, "openai-codex", strlen("openai-codex"), cred, strlen(cred), resp_json, strlen(resp_json), 1789514000000LL); check(report != NULL, "report should not be NULL"); check(strstr(report, "\"planType\":\"prolite\"") != NULL, "codex planType prolite"); check(strstr(report, "\"label\":\"Primary\"") != NULL, "codex Primary limit"); check(strstr(report, "\"status\":\"exhausted\"") != NULL, "codex exhausted status"); check(strstr(report, "\"email\":\"user@example.com\"") != NULL, "codex email"); usage_free(report); free(resp_json); free(body);
char *unknown = usage_advance("nonexistent", strlen("nonexistent"), cred, strlen(cred), "[]", 2, 0); printf("unknown=%s\n", unknown); /* 3. Hyper multi-round state test: team id and name survive across rounds */ usage_state *state = usage_state_new(); check(state != NULL, "usage_state_new failed");
const char *hyper_cred = "{\"kind\":\"apiKey\",\"apiKey\":\"sk-hyper-test\"}"; const char *teams_resp = "[{\"status\":200,\"body\":\"{\\\"items\\\":[{\\\"id\\\":\\\"team_live_1\\\",\\\"name\\\":\\\"Smoke Team\\\"}]}\"}]"; char *hyper_step1 = usage_advance(state, "hyper", strlen("hyper"), hyper_cred, strlen(hyper_cred), teams_resp, strlen(teams_resp), 0); check(hyper_step1 != NULL, "hyper round 1 should succeed"); check(strstr(hyper_step1, "team_live_1/billing") != NULL, "hyper asked for billing"); usage_free(hyper_step1);
const char *billing_resp = "[{\"status\":200,\"body\":\"{\\\"status\\\":\\\"active\\\",\\\"plan\\\":{\\\"name\\\":\\\"Shred\\\",\\\"hypercredits\\\":100}}\"},{\"status\":200,\"body\":\"{\\\"items\\\":[]}\"}]"; char *hyper_step2 = usage_advance(state, "hyper", strlen("hyper"), hyper_cred, strlen(hyper_cred), billing_resp, strlen(billing_resp), 0); check(hyper_step2 != NULL, "hyper round 2 should succeed"); check(strstr(hyper_step2, "\"accountId\":\"team_live_1\"") != NULL, "hyper preserved team_id"); check(strstr(hyper_step2, "Team: Smoke Team") != NULL, "hyper preserved team_name"); usage_free(hyper_step2);
usage_state_free(state);
/* 4. Hyper round 2 with NULL state: does not crash, but cannot emit team name */ char *hyper_no_state = usage_advance(NULL, "hyper", strlen("hyper"), hyper_cred, strlen(hyper_cred), billing_resp, strlen(billing_resp), 0); check(hyper_no_state != NULL, "hyper with NULL state should not crash"); check(strstr(hyper_no_state, "Team: Smoke Team") == NULL, "without state, team name is absent"); usage_free(hyper_no_state);
/* 5. Unknown provider */ char *unknown = usage_advance(NULL, "nonexistent", strlen("nonexistent"), cred, strlen(cred), "[]", 2, 0); check(unknown != NULL && strstr(unknown, "unknown provider") != NULL, "unknown provider handled"); usage_free(unknown);
char *none = usage_advance(NULL, 0, NULL, 0, NULL, 0, 0); printf("null-safe=%s\n", none == NULL ? "null" : "ok"); /* 6. Null-safe advance */ char *none = usage_advance(NULL, NULL, 0, NULL, 0, NULL, 0, 0); check(none != NULL && strstr(none, "unknown provider") != NULL, "null provider handled safely"); usage_free(none);
usage_free(NULL); usage_state_free(NULL);
free(body); printf("c_smoke: all assertions passed\n"); return 0;}3 unmodified lines45678891010111213141516171819202122164 unmodified linesExpand all187188189190191192193194195196190197198199200201202203197204205206207208209210211205212213214215216156 unmodified linesExpand all37337437537637737837938038129 unmodified lines4114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165173 unmodified linesconst iso8601 = usage.iso8601;
const jwt = "eyJhbGciOiJSUzI1NiJ9.eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjdF90ZXN0XzEifSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9wcm9maWxlIjp7ImVtYWlsIjoiVGVzdC5BY2NvdW50QEV4YW1wbGUuQ09NIn19.c2ln";
fn advance(alloc: std.mem.Allocator, provider: []const u8, cred: []const u8, resp: []const u8, now_ms: i64) ![]const u8 {fn advanceWithState(alloc: std.mem.Allocator, state: ?*usage.State, provider: []const u8, cred: []const u8, resp: []const u8, now_ms: i64) ![]const u8 { var w = usage.Writer.init(alloc); const outcome = try usage.advance(alloc, provider, cred, resp, now_ms); const outcome = try usage.advance(alloc, state, provider, cred, resp, now_ms); try usage.writeStep(&w, outcome); return w.items();}
fn advance(alloc: std.mem.Allocator, provider: []const u8, cred: []const u8, resp: []const u8, now_ms: i64) ![]const u8 { return advanceWithState(alloc, null, provider, cred, resp, now_ms);}
test "iso-8601 parser 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 },164 unmodified linesExpand all var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
var state = usage.State.init(a); defer state.deinit();
const cred = "{\"kind\":\"apiKey\",\"apiKey\":\"sk-hyper-123\"}";
// Step 0 -> /v1/teams const step0 = try advance(a, "hyper", cred, "[]", 0); const step0 = try advanceWithState(a, &state, "hyper", cred, "[]", 0); try std.testing.expect(std.mem.indexOf(u8, step0, "https://hyper.charm.land/v1/teams") != null);
// Step 1 -> receives teams, asks for billing and members const teams_resp = \\[{"status":200,"body":"{\"items\":[{\"id\":\"team_123\",\"name\":\"My Team\"}]}"}] ; const step1 = try advance(a, "hyper", cred, teams_resp, 0); const step1 = try advanceWithState(a, &state, "hyper", cred, teams_resp, 0); try std.testing.expect(std.mem.indexOf(u8, step1, "https://hyper.charm.land/v1/teams/team_123/billing") != null); try std.testing.expect(std.mem.indexOf(u8, step1, "https://hyper.charm.land/v1/teams/team_123/members") != null);
// Step 2 -> billing: 237 remaining of 250 credits -> (250-237)/250 = 5.2% used const billing_resp = \\[{"status":200,"body":"{\"status\":\"active\",\"plan\":{\"name\":\"Shred\",\"hypercredits\":250},\"balances\":{\"plan\":{\"hypercredits\":\"237\"},\"total\":{\"usd\":\"11.83\"}},\"hypercredit_refresh\":{\"last_at\":\"2026-09-15T22:26:09Z\",\"next_at\":\"2026-09-16T22:26:09Z\"}}"},{"status":200,"body":"{\"items\":[{\"email\":\"user@example.com\"}]}"}] ; const step2 = try advance(a, "hyper", cred, billing_resp, 0); const step2 = try advanceWithState(a, &state, "hyper", cred, billing_resp, 0); try std.testing.expect(std.mem.indexOf(u8, step2, "\"planType\":\"shred\"") != null); try std.testing.expect(std.mem.indexOf(u8, step2, "\"accountId\":\"team_123\"") != null); try std.testing.expect(std.mem.indexOf(u8, step2, "\"email\":\"user@example.com\"") != null); try std.testing.expect(std.mem.indexOf(u8, step2, "\"usedPercent\":5.2") != null);156 unmodified linesExpand all const resp_127 = "[{\"status\":127,\"body\":\"this host does not run commands (bl)\"}]"; const text = try advance(a, "alibaba", cred, resp_127, 0);
try std.testing.expect(std.mem.indexOf(u8, text, "capture-alibaba-session.mjs") != null); try std.testing.expect(std.mem.indexOf(u8, text, "prime-agent") != null);}
test "alibaba missing sec_token is omitted from form body" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator);29 unmodified lines try std.testing.expect(std.mem.indexOf(u8, text, "Token plan pro: weekly 40000, 5h 12000 credits") != null); try std.testing.expect(std.mem.indexOf(u8, text, "29 days remaining in the plan") != null); try std.testing.expect(std.mem.indexOf(u8, text, "1 reset cards available") != null);}
test "hyper state persists team across rounds even when arena is freed" { var state = usage.State.init(std.testing.allocator); defer state.deinit();
const cred = "{\"kind\":\"apiKey\",\"apiKey\":\"sk-hyper-123\"}"; const teams_resp = \\[{"status":200,"body":"{\"items\":[{\"id\":\"team_live_123\",\"name\":\"Hyper Squad\"}]}"}] ;
// Round 1 in its own arena, which is deinitialized immediately after { var arena1 = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena1.deinit(); const step1 = try advanceWithState(arena1.allocator(), &state, "hyper", cred, teams_resp, 0); try std.testing.expect(std.mem.indexOf(u8, step1, "team_live_123/billing") != null); } // arena1 is now dead and its memory is released
// Round 2 in a fresh arena: must read team_id and team_name from state, not freed memory { var arena2 = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena2.deinit(); const billing_resp = \\[{"status":200,"body":"{\"status\":\"active\",\"plan\":{\"name\":\"Pro\",\"hypercredits\":500},\"balances\":{\"plan\":{\"hypercredits\":\"450\"},\"total\":{\"usd\":\"25.00\"}}}"},{"status":200,"body":"{\"items\":[{\"email\":\"member@example.com\"}]}"}] ; const step2 = try advanceWithState(arena2.allocator(), &state, "hyper", cred, billing_resp, 0); try std.testing.expect(std.mem.indexOf(u8, step2, "\"accountId\":\"team_live_123\"") != null); try std.testing.expect(std.mem.indexOf(u8, step2, "Team: Hyper Squad") != null); try std.testing.expect(std.mem.indexOf(u8, step2, "\"planType\":\"pro\"") != null); }
// Passing NULL state to round 2 does not panic, but cannot emit team name { var arena3 = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena3.deinit(); const billing_resp = \\[{"status":200,"body":"{\"status\":\"active\",\"plan\":{\"name\":\"Pro\",\"hypercredits\":500},\"balances\":{\"plan\":{\"hypercredits\":\"450\"}}}"},{"status":200,"body":"{\"items\":[]}"}] ; const step_no_state = try advanceWithState(arena3.allocator(), null, "hyper", cred, billing_resp, 0); try std.testing.expect(std.mem.indexOf(u8, step_no_state, "Team: Hyper Squad") == null); }}
test "number safety: NaN does not panic integer cast or emit invalid JSON" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const cred = "{\"kind\":\"oauth\",\"access\":\"tok\"}"; const nan_body = \\[{"status":200,"body":"{\"plan_type\":\"plus\",\"rate_limit\":{\"primary_window\":{\"used_percent\":\"nan\",\"reset_at\":\"nan\"}}}"}] ; const text = try advance(a, "openai-codex", cred, nan_body, 0); try std.testing.expect(std.mem.indexOf(u8, text, "\"planType\":\"plus\"") != null); try std.testing.expect(std.mem.indexOf(u8, text, ":nan") == null); try std.testing.expect(std.mem.indexOf(u8, text, ":NaN") == null); _ = try std.json.parseFromSlice(std.json.Value, a, text, .{});}
test "number safety: inf does not serialize as usedPercent:inf" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const cred = "{\"kind\":\"oauth\",\"access\":\"tok\"}"; const inf_body = \\[{"status":200,"body":"{\"plan_type\":\"plus\",\"rate_limit\":{\"primary_window\":{\"used_percent\":\"inf\"}}}"}] ; const text = try advance(a, "openai-codex", cred, inf_body, 0); try std.testing.expect(std.mem.indexOf(u8, text, ":inf") == null); try std.testing.expect(std.mem.indexOf(u8, text, ":Infinity") == null); _ = try std.json.parseFromSlice(std.json.Value, a, text, .{});}
test "number safety: out-of-range 1e30 does not panic integer cast" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const cred = "{\"kind\":\"apiKey\",\"apiKey\":\"key\"}"; const big_body = \\[{"status":200,"body":"{\"code\":200,\"success\":true,\"data\":{\"level\":\"pro\",\"limits\":[{\"type\":\"TIME_LIMIT\",\"percentage\":50,\"nextResetTime\":1e30,\"currentValue\":1e30,\"usage\":1e30}]}}"}] ; const text = try advance(a, "zai", cred, big_body, 0); try std.testing.expect(std.mem.indexOf(u8, text, "\"planType\":\"Pro\"") != null); _ = try std.json.parseFromSlice(std.json.Value, a, text, .{});}
test "number safety: non-number string in numeric field does not panic" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const cred = "{\"kind\":\"apiKey\",\"apiKey\":\"key\"}"; const string_body = \\[{"status":200,"body":"{\"code\":200,\"success\":true,\"data\":{\"level\":\"pro\",\"limits\":[{\"type\":\"TIME_LIMIT\",\"percentage\":\"not-a-number\",\"currentValue\":\"bad\"}]}}"}] ; const text = try advance(a, "zai", cred, string_body, 0); try std.testing.expect(std.mem.indexOf(u8, text, "\"planType\":\"Pro\"") != null); _ = try std.json.parseFromSlice(std.json.Value, a, text, .{});}