From f8c6d45755acb5abf9ac68931268b7945da0fae0 Mon Sep 17 00:00:00 2001 From: t Date: Mon, 15 Jun 2026 22:11:53 -0600 Subject: display fixes: markdown rendering, tool-specific components --- src/pricing_format.zig | 214 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/pricing_format.zig (limited to 'src/pricing_format.zig') diff --git a/src/pricing_format.zig b/src/pricing_format.zig new file mode 100644 index 0000000..daceff9 --- /dev/null +++ b/src/pricing_format.zig @@ -0,0 +1,214 @@ +//! Display formatters for `panto.Pricing` and accumulated session cost. +//! +//! libpanto stores prices as micro-cents per token; the human-friendly +//! unit is USD per million tokens (e.g. "3.0"). The formatters here +//! convert the integer representation to the display form, with two +//! shapes: +//! +//! - `formatPriceCompact` — the dense `"1i/5o/0.1r/1.25w"` form +//! used in the model picker, where the four rates are the only +//! content and the user reads them in a single glance. +//! - `formatCostDollars` — the canonical dollar amount +//! (`"$0.06"`, `"$1.23"`), used in the footer session total. +//! +//! The two have to make a few choices consistently: +//! +//! - Trailing zeros are dropped: "$3.00/Mtok" prints as `"3"`, not +//! `"3.00"`, and the compact form does the same. +//! - Sub-cent values round to the nearest cent, not to 0 or to a +//! half-cent (so `$0.005` becomes `$0.01`, matching what a bank +//! would say). +//! +//! The compact form omits any category whose price is `null` (unknown). +//! When ALL categories are unknown, the result is `null` and the +//! caller renders the bare model id (no price tag). + +const std = @import("std"); +const panto = @import("panto"); + +const Pricing = panto.Pricing; + +/// Suffixes for the compact form, in field order: input, output, +/// cache read, cache write. Known zeros still print (the +/// `cache_write = 0` convention for OpenAI), so the slot's presence +/// is determined by the field, not by its value. +const SUFFIXES = [_]u8{ 'i', 'o', 'r', 'w' }; + +/// The user-facing compact form for a single (provider, model) rate +/// set, e.g. "1i/5o/0.1r/1.25w". Returns `null` when EVERY field is +/// `null` (no pricing known) so the caller can fall back to a +/// model-only label without showing a dangling `?/?/?/?`. +/// +/// Allocation: caller-owned, written into `buf`. The 64-byte buffer +/// is more than enough — the worst case is four fields of up to +/// eight digits plus three slashes plus four suffix bytes. +pub fn formatPriceCompact(pricing: Pricing, buf: []u8) ?[]const u8 { + var out_len: usize = 0; + var emitted: usize = 0; + + const fields = [_]?u64{ pricing.input, pricing.output, pricing.cache_read, pricing.cache_write }; + var i: usize = 0; + while (i < 4) : (i += 1) { + const v = fields[i] orelse continue; + if (emitted != 0) { + if (out_len >= buf.len) return null; + buf[out_len] = '/'; + out_len += 1; + } + var scratch: [16]u8 = undefined; + const text = formatRate(v, &scratch); + if (out_len + text.len + 1 > buf.len) return null; + @memcpy(buf[out_len .. out_len + text.len], text); + out_len += text.len; + buf[out_len] = SUFFIXES[i]; + out_len += 1; + emitted += 1; + } + if (emitted == 0) return null; + return buf[0..out_len]; +} + +/// Format a single micro-cents-per-token value as a human price per +/// million tokens, e.g. 300 -> "3", 30 -> "0.3", 375 -> "3.75", +/// 5 -> "0.05". Strips trailing zeros (and a trailing decimal +/// point) so the result is the shortest accurate representation. +fn formatRate(micro_cents_per_token: u64, buf: []u8) []const u8 { + // We work with the integer directly: `value / 100` is whole + // dollars; `value % 100` is the cents fraction. Render the + // cents, then drop trailing zeros (and the dot if all zeros). + const whole = micro_cents_per_token / 100; + const frac = micro_cents_per_token % 100; + + // 16-byte buffer is more than enough for "99999999.99" (11 chars). + var w: usize = 0; + const whole_text = std.fmt.bufPrint(buf[w..], "{d}", .{whole}) catch return buf[0..0]; + w += whole_text.len; + if (frac != 0) { + buf[w] = '.'; + w += 1; + // Two-digit cents with a leading zero, e.g. 5 -> "05". + const cents_text = std.fmt.bufPrint(buf[w..], "{d:0>2}", .{frac}) catch return buf[0..0]; + w += cents_text.len; + // Drop trailing zeros: "0.30" -> "0.3", "0.05" stays. + while (w > 0 and buf[w - 1] == '0') w -= 1; + // If we dropped the last zero, also drop the dot. + if (w > 0 and buf[w - 1] == '.') w -= 1; + } + return buf[0..w]; +} + +/// Format an accumulated micro-cents total as a dollar amount +/// `"$X.YY"`. Sub-cent values round to the nearest cent; the +/// (rare) case where cents rounding overflows 100 folds to the +/// next dollar. +pub fn formatCostDollars(micro_cents_total: ?u64, buf: []u8) []const u8 { + const total = micro_cents_total orelse return buf[0..0]; + // 1 dollar = 100 cents = 100_000_000 micro-cents. Divide into + // whole dollars and a cents residue, then round the cents to + // the nearest cent. Pure integer arithmetic — no float drift. + const total_cents = total / 1_000_000; // 0.01 USD = 1 cent + const rounding = (total % 1_000_000) / 500_000; // 0 -> no round, 1 -> round up + const cents_total = total_cents + rounding; + const dollars = cents_total / 100; + const cents = cents_total % 100; + return std.fmt.bufPrint(buf, "${d}.{d:0>2}", .{ dollars, cents }) catch buf[0..0]; +} + +// ============================================================================= +// Tests +// ============================================================================= + +const testing = std.testing; + +test "formatPriceCompact: shows all four rates with minimal digits" { + var buf: [64]u8 = undefined; + const p: Pricing = .{ .input = 100, .output = 500, .cache_read = 10, .cache_write = 125 }; + const got = formatPriceCompact(p, &buf).?; + // 100 micro-cents/token = $1.00/Mtok -> "1i" + // 500 -> $5.00 -> "5o" + // 10 -> $0.10 -> "0.1r" + // 125 -> $1.25 -> "1.25w" + try testing.expectEqualStrings("1i/5o/0.1r/1.25w", got); +} + +test "formatPriceCompact: omits unknown fields" { + var buf: [64]u8 = undefined; + const partial: Pricing = .{ .input = 300, .output = 1500 }; + const got = formatPriceCompact(partial, &buf).?; + try testing.expectEqualStrings("3i/15o", got); +} + +test "formatPriceCompact: all-unknown -> null" { + var buf: [64]u8 = undefined; + try testing.expect(formatPriceCompact(.{ .input = null, .output = null, .cache_read = null, .cache_write = null }, &buf) == null); +} + +test "formatPriceCompact: explicit zero prints as 0" { + // OpenAI's cache_write is a known zero, not unknown — must surface as + // "0w" rather than being silently dropped. + var buf: [64]u8 = undefined; + const p: Pricing = .{ .input = 250, .output = 1000, .cache_read = 125, .cache_write = 0 }; + const got = formatPriceCompact(p, &buf).?; + try testing.expectEqualStrings("2.5i/10o/1.25r/0w", got); +} + +test "formatPriceCompact: integer rates strip trailing .00" { + var buf: [64]u8 = undefined; + const p: Pricing = .{ .input = 300, .output = 1500 }; + const got = formatPriceCompact(p, &buf).?; + try testing.expectEqualStrings("3i/15o", got); + // No spurious ".00". + try testing.expect(std.mem.indexOf(u8, got, ".00") == null); +} + +test "formatPriceCompact: sub-cent rate keeps leading zero" { + // 0.05 -> "0.05" (not ".05" or "5e-2"). + var buf: [64]u8 = undefined; + const p: Pricing = .{ .input = 5 }; + const got = formatPriceCompact(p, &buf).?; + try testing.expectEqualStrings("0.05i", got); +} + +test "formatPriceCompact: sub-cent with trailing zero drops it" { + // 0.10 -> "0.1" (the trailing zero is dropped, but the leading 0 stays). + var buf: [64]u8 = undefined; + const p: Pricing = .{ .cache_read = 10 }; + const got = formatPriceCompact(p, &buf).?; + try testing.expectEqualStrings("0.1r", got); +} + +test "formatPriceCompact: example given in the spec (haiku 4.5)" { + // 1i/5o/0.1r/1.25w per the user's example. + var buf: [64]u8 = undefined; + const p: Pricing = .{ .input = 100, .output = 500, .cache_read = 10, .cache_write = 125 }; + const got = formatPriceCompact(p, &buf).?; + try testing.expectEqualStrings("1i/5o/0.1r/1.25w", got); +} + +test "formatCostDollars: zero, sub-cent, whole-dollar, mixed" { + var buf: [32]u8 = undefined; + try testing.expectEqualStrings("$0.00", formatCostDollars(0, &buf)); + // 100_000 micro-cents = $0.001 -> rounded to nearest cent = $0.00. + // (The footer uses "X.YY" form; sub-cent values round.) + try testing.expectEqualStrings("$0.00", formatCostDollars(100_000, &buf)); + // 600_000 micro-cents = $0.006 -> $0.01. + try testing.expectEqualStrings("$0.01", formatCostDollars(600_000, &buf)); + // 6_000_000 micro-cents = $0.06 exactly. + try testing.expectEqualStrings("$0.06", formatCostDollars(6_000_000, &buf)); + // 100_000_000 micro-cents = $1.00. + try testing.expectEqualStrings("$1.00", formatCostDollars(100_000_000, &buf)); + // 123_450_000 micro-cents = $1.2345 -> $1.23. + try testing.expectEqualStrings("$1.23", formatCostDollars(123_450_000, &buf)); +} + +test "formatCostDollars: cent-rounding overflow folds to next dollar" { + // $0.9995 rounds to $1.00; verify the overflow path that nudges the + // dollars counter. + var buf: [32]u8 = undefined; + try testing.expectEqualStrings("$1.00", formatCostDollars(99_950_000, &buf)); +} + +test "formatCostDollars: null total returns empty" { + var buf: [32]u8 = undefined; + try testing.expectEqualStrings("", formatCostDollars(null, &buf)); +} -- cgit v1.3