metergraph-cli 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +60 -11
  2. package/assets/skill/SKILL.md +52 -3
  3. package/assets/skill/manifest.json +3 -3
  4. package/assets/skills/manifest.json +13 -13
  5. package/assets/skills/metergraph-analyze/SKILL.md +62 -4
  6. package/assets/skills/metergraph-investigate/SKILL.md +222 -35
  7. package/assets/skills/metergraph-onboarding/SKILL.md +7 -0
  8. package/assets/skills/metergraph-report/SKILL.md +19 -2
  9. package/bin/metergraph.js +1 -1
  10. package/package.json +1 -1
  11. package/src/agent-key.js +160 -0
  12. package/src/approval-handoff.js +5 -0
  13. package/src/args.js +572 -17
  14. package/src/auth-browser.js +5 -19
  15. package/src/auth-callback.js +24 -15
  16. package/src/auth-login.js +55 -12
  17. package/src/auth-oauth.js +77 -7
  18. package/src/callback-page.js +58 -0
  19. package/src/cli.js +190 -45
  20. package/src/constants.js +73 -3
  21. package/src/environment.js +169 -0
  22. package/src/experiments-contract.js +462 -0
  23. package/src/experiments-output.js +189 -0
  24. package/src/experiments.js +397 -0
  25. package/src/origin.js +6 -0
  26. package/src/output.js +437 -131
  27. package/src/pricing-args.js +163 -0
  28. package/src/pricing-output.js +206 -0
  29. package/src/pricing-reference.js +195 -0
  30. package/src/pricing.js +307 -0
  31. package/src/read-contract.js +976 -16
  32. package/src/read-output.js +449 -30
  33. package/src/read.js +312 -60
  34. package/src/setup-deployment.js +11 -3
  35. package/src/setup-interactive.js +341 -0
  36. package/src/setup-refusal.js +172 -0
  37. package/src/setup-state.js +4 -2
  38. package/src/setup.js +227 -53
  39. package/src/skill-bundle.js +2 -2
  40. package/src/style.js +101 -0
  41. package/src/transport.js +13 -7
  42. package/src/usage-aggregates.js +197 -0
  43. package/src/verify-output.js +21 -11
@@ -0,0 +1,163 @@
1
+ import { DEFAULT_ORIGIN } from "./constants.js";
2
+ import { parseOrigin } from "./origin.js";
3
+ export const PRICING_ACTIONS = ["models", "lookup", "calculate", "cost", "history", "market"];
4
+ const SELECTORS = ["model", "provider", "channel", "region"];
5
+ export const TOKEN_KEYS = [
6
+ "input_tokens",
7
+ "output_tokens",
8
+ "cache_read_tokens",
9
+ "cache_write_tokens",
10
+ "cache_write_5m_tokens",
11
+ "cache_write_1h_tokens",
12
+ ];
13
+ export const PRICING_OPTIONS = [
14
+ "--url",
15
+ "--api",
16
+ "--trace-format",
17
+ "--timeout-ms",
18
+ "--format",
19
+ "--at",
20
+ "--limit",
21
+ ...SELECTORS.map((k) => `--${k}`),
22
+ ...TOKEN_KEYS.map((k) => `--${k.replaceAll("_", "-")}`),
23
+ ];
24
+ export function validDate(v) {
25
+ return (
26
+ typeof v === "string" &&
27
+ !v.startsWith("0000-") &&
28
+ /^\d{4}-\d{2}-\d{2}$/.test(v) &&
29
+ Number.isFinite(Date.parse(v)) &&
30
+ new Date(v).toISOString().slice(0, 10) === v
31
+ );
32
+ }
33
+ export function parsePricing(action, values, flags, json, fail) {
34
+ if (!action)
35
+ return fail("missing_subcommand", "pricing requires models, lookup, calculate, cost, history or market.");
36
+ const origin = parseOrigin(values["--url"] ?? DEFAULT_ORIGIN);
37
+ if (!origin) return fail("invalid_url", "--url requires a bare HTTPS origin, or HTTP on loopback.");
38
+ const api = values["--api"] ?? "public";
39
+ if (!["public", "reference"].includes(api)) return fail("invalid_pricing_api", "--api must be public or reference.");
40
+ if (api === "reference" && !["127.0.0.1", "localhost", "[::1]"].includes(new URL(origin).hostname))
41
+ return fail(
42
+ "reference_requires_loopback",
43
+ "The reference pricing API is local development tooling; use a loopback --url.",
44
+ );
45
+ const cost = action === "cost" || action === "calculate";
46
+ if (api === "reference" && ["models", "calculate"].includes(action))
47
+ return fail(
48
+ "reference_action_unsupported",
49
+ "The reference adapter supports lookup, cost, history and market.",
50
+ "unsupported",
51
+ );
52
+ const format = values["--format"] ?? (action === "history" ? "chart" : "table");
53
+ if (
54
+ !["table", "chart", "html"].includes(format) ||
55
+ (format !== "table" && action !== "history") ||
56
+ (json && values["--format"] !== undefined)
57
+ ) {
58
+ return fail("invalid_format", "Use table, or chart/html for history. --json cannot be combined with --format.");
59
+ }
60
+ const query = {};
61
+ for (const k of SELECTORS) {
62
+ const v = values[`--${k}`];
63
+ if (v !== undefined) {
64
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._/:+-]{0,199}$/.test(v))
65
+ return fail("invalid_selector", "Pricing selectors require 1 to 200 identifier characters.");
66
+ query[k] = k === "model" ? v : v.toLowerCase();
67
+ }
68
+ }
69
+ if (!["market", "models"].includes(action) && !query.model) return fail("missing_model", "--model is required.");
70
+ if (query.provider && query.channel)
71
+ return fail("conflicting_selector", "Choose --provider or --channel; channel selects the exact billing route.");
72
+ if (action === "market" && (query.model || query.provider || !query.channel))
73
+ return fail(
74
+ "invalid_market_selector",
75
+ "market requires --channel and accepts --region; use lookup for a model or provider.",
76
+ );
77
+ if (action === "history" && !query.channel)
78
+ return fail("missing_channel", "history requires --channel to keep one billing route per chart.");
79
+ if (values["--at"] !== undefined || api === "reference")
80
+ query.at = values["--at"] ?? new Date().toISOString().slice(0, 10);
81
+ if (query.at !== undefined && !validDate(query.at))
82
+ return fail("invalid_date", "--at must be a real UTC date, YYYY-MM-DD.");
83
+ if (["history", "market"].includes(action)) query.region ??= "global";
84
+ if (values["--limit"] !== undefined && !["models", "market"].includes(action))
85
+ return fail("unexpected_limit", "--limit is supported only by models and market.");
86
+ if (["market", "models"].includes(action)) {
87
+ const v = values["--limit"] ?? "20";
88
+ if (!/^[1-9]\d?$|^100$/.test(v)) return fail("invalid_limit", "--limit must be 1 to 100.");
89
+ query.limit = Number(v);
90
+ }
91
+ for (const k of TOKEN_KEYS) {
92
+ const v = values[`--${k.replaceAll("_", "-")}`];
93
+ if (v !== undefined) {
94
+ if (!cost || !/^(0|[1-9]\d{0,11})$/.test(v))
95
+ return fail(
96
+ "invalid_usage",
97
+ "Token counts are supported only by cost and must be whole numbers from 0 to 999999999999.",
98
+ );
99
+ query[k] = Number(v);
100
+ }
101
+ }
102
+ if (flags.has("--batch")) {
103
+ if (!cost) return fail("invalid_usage", "--batch is supported only by cost.");
104
+ query.batch = true;
105
+ }
106
+ if (
107
+ query.cache_write_tokens !== undefined &&
108
+ (query.cache_write_5m_tokens !== undefined || query.cache_write_1h_tokens !== undefined)
109
+ )
110
+ return fail("conflicting_usage", "Use an aggregate cache-write count or duration-specific counts, not both.");
111
+ if (action === "models" && query.at !== undefined)
112
+ return fail(
113
+ "models_date_unsupported",
114
+ "models returns current discovery data; --at is not supported.",
115
+ "unsupported",
116
+ );
117
+ const traceFormat =
118
+ values["--trace-format"] ??
119
+ (query.provider === "anthropic" || query.channel === "anthropic-api"
120
+ ? "anthropic.messages"
121
+ : query.provider === "openai" || query.channel === "openai-api"
122
+ ? "openai.responses"
123
+ : "otel.gen_ai");
124
+ if (values["--trace-format"] !== undefined && (!cost || api !== "public"))
125
+ return fail("unexpected_trace_format", "--trace-format is supported only by public calculate/cost.");
126
+ if (!["openai.responses", "openai.chat_completions", "anthropic.messages", "otel.gen_ai"].includes(traceFormat))
127
+ return fail(
128
+ "invalid_trace_format",
129
+ "Use openai.responses, openai.chat_completions, anthropic.messages or otel.gen_ai.",
130
+ );
131
+ if (api === "public" && cost) {
132
+ if (
133
+ (traceFormat.startsWith("openai.") || query.provider === "openai" || query.channel === "openai-api") &&
134
+ query.cache_read_tokens !== undefined &&
135
+ (query.input_tokens === undefined || query.cache_read_tokens > query.input_tokens)
136
+ )
137
+ return fail("invalid_cached_input", "OpenAI cached input must not exceed the supplied total input tokens.");
138
+ if (
139
+ traceFormat !== "anthropic.messages" &&
140
+ TOKEN_KEYS.filter((k) => k.startsWith("cache_write")).some((k) => query[k] !== undefined)
141
+ )
142
+ return fail(
143
+ "cache_write_format_unsupported",
144
+ "Use --trace-format anthropic.messages for cache-write counts; other mappings are not verified.",
145
+ "unsupported",
146
+ );
147
+ }
148
+ const raw = values["--timeout-ms"] ?? "10000";
149
+ if (!/^\d+$/.test(raw) || Number(raw) < 100 || Number(raw) > 30000)
150
+ return fail("invalid_timeout", "--timeout-ms must be 100 to 30000.");
151
+ return {
152
+ ok: true,
153
+ command: "pricing",
154
+ action,
155
+ origin,
156
+ query,
157
+ format,
158
+ api,
159
+ traceFormat,
160
+ timeoutMs: Number(raw),
161
+ json,
162
+ };
163
+ }
@@ -0,0 +1,206 @@
1
+ // All terminal text arrives through the bounded pricing validator. HTML escapes
2
+ // every dynamic label; no scripts, remote fonts, analytics or external assets.
3
+ const money = (v) => (v === null ? "unknown" : Number(v).toLocaleString("en-US", { maximumFractionDigits: 8 }));
4
+ const escape = (s) =>
5
+ String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
6
+ function movement(s) {
7
+ const points = s.points;
8
+ const now = points.at(-1),
9
+ before = points.at(-2);
10
+ if (
11
+ !before ||
12
+ Date.parse(before.effective_to) !== Date.parse(now.effective_from) ||
13
+ before.input_per_mtok === null ||
14
+ now.input_per_mtok === null ||
15
+ Number(before.input_per_mtok) === 0
16
+ )
17
+ return "n/a";
18
+ const delta = (Number(now.input_per_mtok) / Number(before.input_per_mtok) - 1) * 100;
19
+ return `${delta > 0 ? "+" : ""}${delta.toFixed(1)}%`;
20
+ }
21
+ function spark(points) {
22
+ const nums = points.map((p) => (p.input_per_mtok === null ? null : Number(p.input_per_mtok)));
23
+ const known = nums.filter((n) => n !== null);
24
+ const min = Math.min(...known),
25
+ max = Math.max(...known);
26
+ return nums
27
+ .map((n) => (n === null ? "?" : "▁▂▃▄▅▆▇█"[max === min ? 3 : Math.round(((n - min) / (max - min)) * 7)]))
28
+ .join("");
29
+ }
30
+ function provenance(d) {
31
+ if (d.api === "public")
32
+ return d.catalog
33
+ ? `Catalog ${d.catalog.version} · verified ${d.catalog.pricing_verified_at} · discovery as of ${d.as_of}\n${d.action === "market" ? "Rates were obtained through separate lookups; public history is unavailable." : "Discovery lists supported dimensions, not numeric rates. Use pricing lookup."}\n`
34
+ : "Public pricing result. The API does not report catalog version or verification time on this response.\nCatalog list rates, not your invoice.\n";
35
+ return `Catalog ${d.catalog.version} · verified ${d.catalog.pricing_verified_at} · served ${d.catalog.served_at}\nCatalog list rates, not your invoice. Base rates exclude conditional rules; use pricing cost.\n`;
36
+ }
37
+ export function pricingText(e, format, style) {
38
+ if (!e.ok) return `Pricing unavailable\n${e.error.message}\nReason: ${e.error.reason}\n`;
39
+ const d = e.data,
40
+ r = d.result;
41
+ let lines = [
42
+ style.bold(`METERGRAPH / PRICING ${d.action.toUpperCase()}`),
43
+ d.request.at ? `As of ${d.request.at} UTC` : `Effective time: ${r.effective_at ?? d.as_of}`,
44
+ "",
45
+ ];
46
+ if (d.action === "models") {
47
+ lines.push(
48
+ `Canonical models: ${r.total_models}; matching: ${r.matched}; showing: ${r.models.length}${r.truncated ? " (raise --limit for more)" : ""}`,
49
+ `Formats: ${d.formats.join(", ")}`,
50
+ "",
51
+ "MODEL PUBLISHER CHANNELS",
52
+ );
53
+ for (const m of r.models)
54
+ lines.push(
55
+ `${m.model_id.padEnd(38)} ${m.publisher.padEnd(15)} ${[...new Set(m.prices.map((p) => p.channel))].join(", ")}`,
56
+ );
57
+ lines.push("", "Filters are applied locally to current discovery metadata. Numeric rates require pricing lookup.");
58
+ } else if (d.action === "market" && d.api === "public") {
59
+ lines.push("MODEL STATUS CCY INPUT OUTPUT");
60
+ for (const item of r.items) {
61
+ const p = item.price;
62
+ lines.push(
63
+ `${(item.model_id ?? item.requested_model).padEnd(38)} ${item.status.padEnd(9)} ${(item.currency ?? "---").padEnd(3)} ${money(p?.input_per_mtok ?? null).padStart(10)} ${money(p?.output_per_mtok ?? null).padStart(10)}`,
64
+ );
65
+ if (item.missing_dimensions.length) lines.push(` Missing: ${item.missing_dimensions.join(", ")}`);
66
+ if (p && d.request.region !== p.region)
67
+ lines.push(` Requested region: ${d.request.region} · resolved region: ${p.region} (fallback)`);
68
+ }
69
+ lines.push(
70
+ "",
71
+ `${d.request.channel} / ${d.request.region} · base rates per million tokens`,
72
+ `Showing ${r.items.length} of ${r.matched} matching current models${r.truncated ? " · raise --limit for more" : ""}`,
73
+ "Current discovery plus bounded lookups, not an atomic price snapshot. History, returns and sparklines are unavailable from the public API.",
74
+ );
75
+ } else if (d.action === "market") {
76
+ lines.push("MODEL CCY INPUT OUTPUT LAST INPUT CHANGE HISTORY");
77
+ for (const s of r.series) {
78
+ const p = s.points.at(-1);
79
+ lines.push(
80
+ `${s.model_id.padEnd(38)} ${s.currency} ${money(p.input_per_mtok).padStart(8)} ${money(p.output_per_mtok).padStart(10)} ${movement(s).padStart(12)} ${spark(s.points)}`,
81
+ );
82
+ }
83
+ lines.push(
84
+ "",
85
+ `${d.request.channel} / ${d.request.region} · rates per million tokens`,
86
+ `Showing ${r.series.length} of ${r.matched} models${r.truncated ? " · raise --limit for more" : ""}`,
87
+ "Change is between the last two contiguous catalog windows, not a 24h return. Sparklines show events, not elapsed time.",
88
+ );
89
+ } else if (d.action === "history") {
90
+ const s = r.series[0];
91
+ if (!s) lines.push("No catalog history for this exact model/channel/region.");
92
+ else {
93
+ lines.push(`${s.model_id} · ${s.channel} / ${s.region} · ${s.currency} per million tokens`);
94
+ if (format === "chart") lines.push(...terminalChart(s, d.request.at));
95
+ lines.push("", "EFFECTIVE FROM INPUT OUTPUT UNTIL");
96
+ for (const p of s.points)
97
+ lines.push(
98
+ `${p.effective_from.slice(0, 19)} ${money(p.input_per_mtok).padStart(10)} ${money(p.output_per_mtok).padStart(10)} ${p.effective_to ?? "open"}`,
99
+ );
100
+ lines.push("", `Last input change: ${movement(s)} · effective windows; gaps mean unknown pricing.`);
101
+ }
102
+ } else {
103
+ lines.push(
104
+ `Status: ${r.status}${r.missing_dimensions.length ? ` · missing ${r.missing_dimensions.join(", ")}` : ""}`,
105
+ );
106
+ if (r.price)
107
+ lines.push(
108
+ `${r.model_id} · ${r.price.channel} / ${r.price.region}`,
109
+ `Input ${money(r.price.input_per_mtok)} · Output ${money(r.price.output_per_mtok)} ${r.price.currency}/million tokens`,
110
+ `Source: ${r.price.source_url}`,
111
+ ...(d.request.region && d.request.region !== r.price.region
112
+ ? [`Requested region: ${d.request.region} · resolved price region: ${r.price.region} (fallback)`]
113
+ : []),
114
+ );
115
+ if (["cost", "calculate"].includes(d.action))
116
+ lines.push(
117
+ `Cost: ${r.cost ?? "unknown"} ${r.currency ?? ""}${r.status === "partial" ? " (known subtotal)" : ""}`,
118
+ ...(r.components ? Object.entries(r.components).map(([k, v]) => ` ${k}: ${v}`) : []),
119
+ `Reasons: ${r.reasons?.join(", ") || (d.api === "public" ? "not reported" : "none")}`,
120
+ );
121
+ }
122
+ return `${lines.join("\n")}\n\n${provenance(d)}`;
123
+ }
124
+ function domain(s, at) {
125
+ const end = Date.parse(`${at}T00:00:00Z`);
126
+ const start = Math.min(Date.parse(s.points[0].effective_from), end - 86400000);
127
+ return {
128
+ start,
129
+ end,
130
+ max: Math.max(
131
+ 0.00000001,
132
+ ...s.points
133
+ .flatMap((p) => [p.input_per_mtok, p.output_per_mtok])
134
+ .filter((v) => v !== null)
135
+ .map(Number),
136
+ ),
137
+ };
138
+ }
139
+ function terminalChart(s, at) {
140
+ const { start, end, max } = domain(s, at);
141
+ const grid = Array.from({ length: 9 }, () => Array(55).fill(" "));
142
+ for (let x = 0; x < 55; x++) {
143
+ const t = start + ((end - start) * x) / 54;
144
+ const p = s.points.find(
145
+ (p) => Date.parse(p.effective_from) <= t && (p.effective_to === null || Date.parse(p.effective_to) > t),
146
+ );
147
+ if (!p) continue;
148
+ for (const [key, glyph] of [
149
+ ["output_per_mtok", "o"],
150
+ ["input_per_mtok", "i"],
151
+ ]) {
152
+ if (p[key] === null) continue;
153
+ const y = 8 - Math.round((Number(p[key]) / max) * 8);
154
+ grid[y][x] = grid[y][x] === " " ? glyph : "*";
155
+ }
156
+ }
157
+ return [
158
+ "",
159
+ ...grid.map((row, i) => `${money((max * (8 - i)) / 8).padStart(10)} │${row.join("")}`),
160
+ ` └${"─".repeat(55)}`,
161
+ ` ${new Date(start).toISOString().slice(0, 10)}${" ".repeat(35)}${at}`,
162
+ " i input · o output · * overlap (linear UTC time)",
163
+ ];
164
+ }
165
+ export function pricingHtml(d) {
166
+ const s = d.result.series[0];
167
+ if (!s)
168
+ return "<!doctype html><meta charset=utf-8><title>Pricing history</title><p>No matching catalog history.</p>\n";
169
+ const { start, end, max } = domain(s, d.request.at),
170
+ x = (t) => 70 + ((Date.parse(t) - start) / (end - start)) * 850,
171
+ y = (v) => 330 - (Number(v) / max) * 250;
172
+ const paths = [];
173
+ for (const [key, color] of [
174
+ ["input_per_mtok", "#46e3bb"],
175
+ ["output_per_mtok", "#aaa4ff"],
176
+ ]) {
177
+ for (const [index, p] of s.points.entries()) {
178
+ if (p[key] === null) continue;
179
+ const until = Math.min(end, p.effective_to ? Date.parse(p.effective_to) : end);
180
+ if (until < Date.parse(p.effective_from)) continue;
181
+ const next = s.points[index + 1];
182
+ const join =
183
+ next && Date.parse(p.effective_to) === Date.parse(next.effective_from) && next[key] !== null
184
+ ? `V${y(next[key])}`
185
+ : "";
186
+ paths.push(
187
+ `<path d="M${x(p.effective_from)},${y(p[key])}H${70 + ((until - start) / (end - start)) * 850}${join}" stroke="${color}" stroke-width="3" fill="none"/><circle cx="${x(p.effective_from)}" cy="${y(p[key])}" r="3" fill="${color}"><title>${escape(p.effective_from)}: ${money(p[key])}</title></circle>`,
188
+ );
189
+ }
190
+ }
191
+ const last = s.points.at(-1);
192
+ const rows = s.points
193
+ .map(
194
+ (p) =>
195
+ `<tr><td>${escape(p.effective_from)}</td><td>${escape(p.effective_to ?? "open")}</td><td>${money(p.input_per_mtok)}</td><td>${money(p.output_per_mtok)}</td><td><a href="${escape(p.source_url)}">Provider source</a></td></tr>`,
196
+ )
197
+ .join("");
198
+ return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escape(s.model_id)} · Metergraph price history</title>
199
+ <style>body{margin:0;background:#10151e;color:#eef2f8;font:16px system-ui}main{max-width:1050px;margin:55px auto;padding:24px}header,section{border-bottom:1px solid #303847;padding:24px 0}small,p{color:#acb8ca}h1{font-size:40px;margin:14px 0}strong{font-size:32px}svg{width:100%;height:auto} .table-scroll{overflow-x:auto}table{width:100%;text-align:left;border-collapse:collapse}td,th{padding:12px;border-bottom:1px solid #303847}a{color:#46e3bb}.metrics{display:flex;gap:50px;flex-wrap:wrap}.input{color:#46e3bb}.output{color:#aaa4ff}</style>
200
+ <main><header><small>METERGRAPH / MODEL PRICE HISTORY · ${escape(d.request.at)} UTC</small><h1>${escape(s.model_id)}</h1><p>${escape(s.channel)} / ${escape(s.region)} · ${escape(s.currency)} per million tokens</p></header>
201
+ <section class="metrics"><div class="input">Last recorded input<br><strong>${money(last.input_per_mtok)}</strong></div><div class="output">Last recorded output<br><strong>${money(last.output_per_mtok)}</strong></div><div>Last input change<br><strong>${movement(s)}</strong></div></section>
202
+ <svg role="img" aria-labelledby="title desc" viewBox="0 0 1000 400"><title id="title">Effective-dated input and output base rates</title><desc id="desc">Horizontal segments cover known pricing windows. Gaps are unknown. Exact values and sources follow in the table.</desc><g fill="#acb8ca" font-family="system-ui" font-size="14"><text x="10" y="85">${money(max)}</text><text x="45" y="335">0</text><text x="70" y="370">${new Date(start).toISOString().slice(0, 10)}</text><text x="840" y="370">${escape(d.request.at)}</text></g><path d="M70 80V330H920" stroke="#435064" fill="none"/>${paths.join("")}</svg>
203
+ <p><span class="input">━ Input</span> &nbsp; <span class="output">━ Output</span> · Linear UTC time; effective windows, not daily observations. Gaps mean unknown.</p>
204
+ <div class="table-scroll"><table><caption>Catalog price windows and provider sources</caption><thead><tr><th>From (UTC)</th><th>Until (exclusive)</th><th>Input</th><th>Output</th><th>Source</th></tr></thead><tbody>${rows}</tbody></table></div>
205
+ <p>${escape(provenance(d))}</p><footer>Explore your application's actual model costs with <a href="https://www.metergraph.dev/">Metergraph</a>. Free pricing queries require no account.</footer></main></html>\n`;
206
+ }
@@ -0,0 +1,195 @@
1
+ import { send } from "./transport.js";
2
+ import { parseJsonObject } from "./http.js";
3
+ import { validDate } from "./pricing-args.js";
4
+ export const PRICING_SCHEMA = "metergraph.pricing/v1";
5
+ const fail = (outcome, reason) => ({
6
+ outcome,
7
+ reason,
8
+ message: "Pricing could not be read. Check the reason and the pricing service contract.",
9
+ });
10
+ const text = (v) =>
11
+ typeof v === "string" &&
12
+ v.length > 0 &&
13
+ v.length <= 500 &&
14
+ !/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/u.test(v);
15
+ const currency = (v) => typeof v === "string" && /^[A-Z]{3}$/.test(v);
16
+ const decimal = (v) =>
17
+ v === null || (typeof v === "string" && /^(0|[1-9]\d{0,14})(\.\d{1,16})?$/.test(v) && Number.isFinite(Number(v)));
18
+ const instant = (v) =>
19
+ typeof v === "string" &&
20
+ /^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,6})?(?:Z|\+00:00)$/.test(v) &&
21
+ validDate(v.slice(0, 10)) &&
22
+ Number.isFinite(Date.parse(v));
23
+ const source = (v) => {
24
+ if (!text(v)) return false;
25
+ try {
26
+ const u = new URL(v);
27
+ return u.protocol === "https:" && !u.username && !u.password;
28
+ } catch {
29
+ return false;
30
+ }
31
+ };
32
+ export const RATE_KEYS = [
33
+ "input_per_mtok",
34
+ "output_per_mtok",
35
+ "cache_read_per_mtok",
36
+ "cache_write_5m_per_mtok",
37
+ "cache_write_1h_per_mtok",
38
+ "batch_input_per_mtok",
39
+ "batch_output_per_mtok",
40
+ "request_fee_per_1k",
41
+ ];
42
+ function price(p) {
43
+ return (
44
+ p &&
45
+ ["id", "model_id", "channel", "region"].every((k) => text(p[k])) &&
46
+ currency(p.currency) &&
47
+ instant(p.effective_from) &&
48
+ (p.effective_to === null ||
49
+ (instant(p.effective_to) && Date.parse(p.effective_to) > Date.parse(p.effective_from))) &&
50
+ source(p.source_url) &&
51
+ RATE_KEYS.every((k) => decimal(p[k])) &&
52
+ p.rules &&
53
+ typeof p.rules === "object" &&
54
+ !Array.isArray(p.rules)
55
+ );
56
+ }
57
+ function series(s, q) {
58
+ if (
59
+ !s ||
60
+ !["model_id", "channel", "region"].every((k) => text(s[k])) ||
61
+ s.channel !== q.channel ||
62
+ s.region !== q.region ||
63
+ !currency(s.currency) ||
64
+ !Array.isArray(s.points) ||
65
+ s.points.length < 1 ||
66
+ s.points.length > 1000
67
+ )
68
+ return false;
69
+ return s.points.every(
70
+ (p, i) =>
71
+ price(p) &&
72
+ p.model_id === s.model_id &&
73
+ p.channel === s.channel &&
74
+ p.region === s.region &&
75
+ p.currency === s.currency &&
76
+ Date.parse(p.effective_from) <= Date.parse(`${q.at}T00:00:00Z`) &&
77
+ (i === 0 ||
78
+ (Date.parse(s.points[i - 1].effective_from) < Date.parse(p.effective_from) &&
79
+ s.points[i - 1].effective_to !== null &&
80
+ Date.parse(s.points[i - 1].effective_to) <= Date.parse(p.effective_from))),
81
+ );
82
+ }
83
+ export function validatePricing(d, action, q) {
84
+ if (
85
+ !d ||
86
+ d.schema_version !== PRICING_SCHEMA ||
87
+ d.action !== action ||
88
+ !d.request ||
89
+ JSON.stringify(Object.keys(d.request).sort()) !== JSON.stringify(Object.keys(q).sort()) ||
90
+ !Object.keys(q).every((k) => d.request[k] === q[k]) ||
91
+ !d.catalog ||
92
+ !text(d.catalog.version) ||
93
+ typeof d.catalog.content_hash !== "string" ||
94
+ !/^[a-f0-9]{64}$/.test(d.catalog.content_hash) ||
95
+ !validDate(d.catalog.pricing_verified_at) ||
96
+ !instant(d.catalog.served_at) ||
97
+ !text(d.catalog.core_version)
98
+ )
99
+ return false;
100
+ const r = d.result;
101
+ if (!r) return false;
102
+ if (action === "market" || action === "history") {
103
+ if (
104
+ !Array.isArray(r.series) ||
105
+ r.series.length > (action === "history" ? 1 : q.limit) ||
106
+ !r.series.every((s) => series(s, q)) ||
107
+ typeof r.truncated !== "boolean" ||
108
+ !Number.isSafeInteger(r.matched) ||
109
+ r.matched < r.series.length ||
110
+ r.truncated !== r.matched > r.series.length ||
111
+ (action === "history" && r.truncated)
112
+ )
113
+ return false;
114
+ if (
115
+ action === "market" &&
116
+ r.series.some(
117
+ (s) =>
118
+ s.points.at(-1).effective_to !== null &&
119
+ Date.parse(s.points.at(-1).effective_to) <= Date.parse(`${q.at}T00:00:00Z`),
120
+ )
121
+ )
122
+ return false;
123
+ const ids = r.series.map((s) => `${s.model_id}|${s.channel}|${s.region}`);
124
+ return new Set(ids).size === ids.length;
125
+ }
126
+ if (
127
+ !["priced", "partial", "unpriced"].includes(r.status) ||
128
+ !Array.isArray(r.missing_dimensions) ||
129
+ new Set(r.missing_dimensions).size !== r.missing_dimensions.length ||
130
+ !r.missing_dimensions.every((k) => ["region", "channel", "input_tokens", "output_tokens"].includes(k)) ||
131
+ !instant(r.effective_at) ||
132
+ Date.parse(r.effective_at) !== Date.parse(`${q.at}T00:00:00Z`)
133
+ )
134
+ return false;
135
+ if (r.status === "unpriced") {
136
+ if (r.price !== null || r.model_id !== null) return false;
137
+ } else if (
138
+ !price(r.price) ||
139
+ r.model_id !== r.price.model_id ||
140
+ (q.channel && r.price.channel !== q.channel) ||
141
+ (q.region && ![q.region, "global", "*"].includes(r.price.region)) ||
142
+ Date.parse(r.price.effective_from) > Date.parse(r.effective_at) ||
143
+ (r.price.effective_to !== null && Date.parse(r.price.effective_to) <= Date.parse(r.effective_at))
144
+ )
145
+ return false;
146
+ if (r.status === "priced" && r.missing_dimensions.length) return false;
147
+ if (action === "cost") {
148
+ if (!decimal(r.cost) || !Array.isArray(r.reasons) || !r.reasons.every(text)) return false;
149
+ if (r.status === "priced" && r.reasons.length) return false;
150
+ if (
151
+ r.status !== "unpriced" &&
152
+ ["input_tokens", "output_tokens"].some((k) => !Object.hasOwn(q, k) && !r.missing_dimensions.includes(k))
153
+ )
154
+ return false;
155
+ if (r.status === "unpriced") return r.cost === null && r.components === null && r.currency === null;
156
+ if (
157
+ r.cost === null ||
158
+ r.currency !== r.price.currency ||
159
+ !r.components ||
160
+ !["input", "output", "cache_read", "cache_write", "request"].every(
161
+ (k) => r.components[k] !== null && decimal(r.components[k]),
162
+ )
163
+ )
164
+ return false;
165
+ // Validate the engine's rounded breakdown using integer decimal arithmetic.
166
+ const units = (value) => {
167
+ const [whole, fraction = ""] = value.split(".");
168
+ return BigInt(whole) * 10n ** 16n + BigInt(fraction.padEnd(16, "0"));
169
+ };
170
+ if (
171
+ Object.keys(r.components).sort().join() !== "cache_read,cache_write,input,output,request" ||
172
+ Object.values(r.components).reduce((sum, v) => sum + units(v), 0n) !== units(r.cost)
173
+ )
174
+ return false;
175
+ }
176
+ return true;
177
+ }
178
+ export async function runReferencePricing(o) {
179
+ const query = new URLSearchParams(Object.entries(o.query).map(([k, v]) => [k, String(v)]));
180
+ const res = await send(o.origin, `/v1/pricing/${o.action}`, {
181
+ query,
182
+ maxBytes: 1024 * 1024,
183
+ signal: AbortSignal.timeout(o.timeoutMs),
184
+ });
185
+ if (res.kind === "error") return fail("connection_failed", res.reason);
186
+ if (res.status >= 300 && res.status < 400) return fail("redirect_rejected", "pricing_redirect_rejected");
187
+ if (res.status === 429) return fail("rate_limited", "pricing_rate_limited");
188
+ if (res.status >= 500) return fail("unhealthy", "pricing_service_unhealthy");
189
+ if ([401, 403, 404, 405].includes(res.status)) return fail("unsupported", "public_pricing_unavailable");
190
+ if (res.tooLarge) return fail("unsupported", "pricing_response_too_large");
191
+ if (res.status !== 200) return fail("unsupported", "pricing_response_unsupported");
192
+ const data = parseJsonObject(res);
193
+ if (!validatePricing(data, o.action, o.query)) return fail("verification_failed", "pricing_contract_mismatch");
194
+ return { outcome: "ok", data };
195
+ }