diff options
Diffstat (limited to 'libpanto/src/pricing.zig')
| -rw-r--r-- | libpanto/src/pricing.zig | 87 |
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 // ============================================================================= |
