summaryrefslogtreecommitdiff
path: root/libpanto/src/pricing.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-15 22:11:53 -0600
committert <t@tjp.lol>2026-06-16 11:17:14 -0600
commitf8c6d45755acb5abf9ac68931268b7945da0fae0 (patch)
treea88ed7edd4aa3e5acced9ff99ac61b7f36b267a6 /libpanto/src/pricing.zig
parent8206642d3a1736aaf928a5b9a732e747f5294228 (diff)
display fixes: markdown rendering, tool-specific components
Diffstat (limited to 'libpanto/src/pricing.zig')
-rw-r--r--libpanto/src/pricing.zig87
1 files changed, 87 insertions, 0 deletions
diff --git a/libpanto/src/pricing.zig b/libpanto/src/pricing.zig
index 7d4e1f8..97dfe38 100644
--- a/libpanto/src/pricing.zig
+++ b/libpanto/src/pricing.zig
@@ -83,6 +83,13 @@ pub const Pricing = struct {
if (r >= @as(f64, @floatFromInt(std.math.maxInt(u64)))) return std.math.maxInt(u64);
return @intFromFloat(r);
}
+
+ /// Inverse of `fromDollarsPerMtok`: convert the integer representation
+ /// back to USD per million tokens (the human-friendly unit). Returns
+ /// 0.0 for the null case.
+ pub fn toDollarsPerMtok(micro_cents_per_token: u64) f64 {
+ return @as(f64, @floatFromInt(micro_cents_per_token)) / 100.0;
+ }
};
// =============================================================================
@@ -120,6 +127,86 @@ fn component(tokens: u64, price: ?u64) ?u64 {
return tokens *% p;
}
+/// Add a turn's cost to a running session total, in micro-cents.
+/// Saturating `+%` on the total (a session-bucket overflow is
+/// catastrophic for a `u64` value, so we pin to max rather than wrap).
+/// The poison rule matches `costMicroCents`: any individual turn
+/// whose cost is unknown poisons the session total to `null`.
+///
+/// This is the single accumulation point for session-level cost
+/// display, so the model-switch tolerance lives here: a switch in the
+/// middle of a session just means successive `costMicroCents` calls
+/// run against different `Pricing` structs (one per `(provider,
+/// model)`). The TUI footer's cost pass holds the registry and looks
+/// up the right pricing for each turn at the time the turn lands.
+pub fn addCost(total: ?u64, turn_cost: ?u64) ?u64 {
+ const t = total orelse return null;
+ const c = turn_cost orelse return null;
+ return t +% c;
+}
+
+test "addCost: accumulates known costs across many turns" {
+ var s: ?u64 = 0;
+ s = addCost(s, 1_000_000); // $0.01
+ s = addCost(s, 5_000_000); // $0.05
+ s = addCost(s, 2_000_000); // $0.02
+ try testing.expectEqual(@as(?u64, 8_000_000), s);
+}
+
+test "addCost: a known + unknown + known sequence poisons the total" {
+ // The poison rule is one-way: once any priced component of any
+ // turn is unknown, the whole session cost is unknown forever.
+ var s: ?u64 = 0;
+ s = addCost(s, 1_000_000); // $0.01 (known)
+ try testing.expectEqual(@as(?u64, 1_000_000), s);
+ s = addCost(s, null); // poison!
+ try testing.expect(s == null);
+ s = addCost(s, 5_000_000); // still null
+ try testing.expect(s == null);
+}
+
+test "addCost: tolerates a model switch mid-session (each turn's cost is per-model)" {
+ // A model switch in the middle of a session: each turn's
+ // cost is computed against the active model's pricing
+ // upstream of `addCost`, so the function itself just sees a
+ // sequence of independent turn costs. The TUI's session-cost
+ // display sums them all; this is the tolerance: a switch is
+ // invisible to the accumulator as long as both models have
+ // pricing entries.
+ var s: ?u64 = 0;
+ s = addCost(s, costMicroCents(
+ .{ .input = 100, .output = 50 },
+ .{ .input = 300, .output = 1500 },
+ ).?);
+ // Switch to a different-priced model.
+ s = addCost(s, costMicroCents(
+ .{ .input = 200, .output = 100 },
+ .{ .input = 100, .output = 500 },
+ ).?);
+ // 100*300 + 50*1500 = 105_000
+ // 200*100 + 100*500 = 70_000
+ // total = 175_000 micro-cents = $0.00175 -> $0.00 (rounded)
+ try testing.expectEqual(@as(?u64, 175_000), s);
+}
+
+test "addCost: a switch from a priced model to an unpriced model poisons" {
+ // The new model has no pricing entry: `costMicroCents` returns
+ // null (every priced component is null), and `addCost` then
+ // poisons the session total to null. The cost UP TO the
+ // switch is "known"; after it the total is "unknown".
+ var s: ?u64 = 0;
+ s = addCost(s, costMicroCents(
+ .{ .input = 100, .output = 50 },
+ .{ .input = 300, .output = 1500 },
+ ).?);
+ // Switch to an unpriced model.
+ s = addCost(s, costMicroCents(
+ .{ .input = 200, .output = 100 },
+ .{}, // no pricing at all
+ ));
+ try testing.expect(s == null);
+}
+
// =============================================================================
// Registry
// =============================================================================