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
package/src/pricing.js ADDED
@@ -0,0 +1,307 @@
1
+ import { send } from "./transport.js";
2
+ import { parseJsonObject } from "./http.js";
3
+ import { validDate } from "./pricing-args.js";
4
+ import { runReferencePricing, RATE_KEYS } from "./pricing-reference.js";
5
+
6
+ const fail = (
7
+ outcome,
8
+ reason,
9
+ message = "Pricing could not be read. Check the reason and retry with supported inputs.",
10
+ ) => ({ outcome, reason, message });
11
+ const text = (v) =>
12
+ typeof v === "string" &&
13
+ v.length > 0 &&
14
+ v.length <= 500 &&
15
+ !/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/u.test(v);
16
+ const currency = (v) => typeof v === "string" && /^[A-Z]{3}$/.test(v);
17
+ const decimal = (v) => typeof v === "string" && /^(0|[1-9]\d{0,14})(\.\d{1,16})?$/.test(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 date = (v) => validDate(v) || instant(v);
24
+ const strings = (v) => Array.isArray(v) && v.length <= 10000 && v.every(text);
25
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
26
+ const source = (v) => {
27
+ if (!text(v)) return false;
28
+ try {
29
+ const u = new URL(v);
30
+ return u.protocol === "https:" && !u.username && !u.password;
31
+ } catch {
32
+ return false;
33
+ }
34
+ };
35
+ const units = (v) => {
36
+ const [a, b = ""] = v.split(".");
37
+ return BigInt(a) * 10n ** 16n + BigInt(b.padEnd(16, "0"));
38
+ };
39
+ function price(p) {
40
+ return (
41
+ object(p) &&
42
+ ["id", "model_id", "channel", "region"].every((k) => text(p[k])) &&
43
+ currency(p.currency) &&
44
+ date(p.effective_from) &&
45
+ (p.effective_to === null || (date(p.effective_to) && Date.parse(p.effective_to) > Date.parse(p.effective_from))) &&
46
+ source(p.source_url) &&
47
+ object(p.rules) &&
48
+ RATE_KEYS.every((k) => p[k] === null || decimal(p[k]))
49
+ );
50
+ }
51
+ export function validateModels(d) {
52
+ return (
53
+ object(d) &&
54
+ instant(d.as_of) &&
55
+ object(d.catalog) &&
56
+ text(d.catalog.version) &&
57
+ currency(d.catalog.currency) &&
58
+ validDate(d.catalog.pricing_verified_at) &&
59
+ ["formats", "providers", "channels", "regions", "service_tiers"].every((k) => strings(d[k])) &&
60
+ Array.isArray(d.models) &&
61
+ d.models.length <= 10000 &&
62
+ new Set(d.models.map((m) => m?.model_id)).size === d.models.length &&
63
+ d.models.every(
64
+ (m) =>
65
+ object(m) &&
66
+ text(m.model_id) &&
67
+ text(m.publisher) &&
68
+ Array.isArray(m.aliases) &&
69
+ m.aliases.every((a) => object(a) && ["provider", "model", "channel"].every((k) => text(a[k]))) &&
70
+ Array.isArray(m.prices) &&
71
+ m.prices.every(
72
+ (p) =>
73
+ object(p) &&
74
+ text(p.channel) &&
75
+ text(p.region) &&
76
+ currency(p.currency) &&
77
+ date(p.effective_from) &&
78
+ strings(p.dimensions) &&
79
+ strings(p.service_tiers),
80
+ ),
81
+ )
82
+ );
83
+ }
84
+ export function validatePublicResult(d, action, q, now = Date.now()) {
85
+ if (!object(d) || !Array.isArray(d.results) || d.results.length !== 1) return false;
86
+ const r = d.results[0];
87
+ if (
88
+ !object(r) ||
89
+ !["priced", "partial", "unpriced"].includes(r.status) ||
90
+ !instant(r.effective_at) ||
91
+ !strings(r.missing_dimensions) ||
92
+ new Set(r.missing_dimensions).size !== r.missing_dimensions.length
93
+ )
94
+ return false;
95
+ if (
96
+ q.at ? Date.parse(r.effective_at) !== Date.parse(q.at) : Math.abs(Date.parse(r.effective_at) - now) > 15 * 60 * 1000
97
+ )
98
+ return false;
99
+ if (r.status === "priced" && r.missing_dimensions.length) return false;
100
+ if (r.status === "unpriced") {
101
+ if (r.model_id !== null || r.price !== null || r.currency !== null) return false;
102
+ } else if (
103
+ !price(r.price) ||
104
+ r.model_id !== r.price.model_id ||
105
+ r.currency !== r.price.currency ||
106
+ (q.channel && r.price.channel !== q.channel) ||
107
+ (q.region && ![q.region, "global", "*"].includes(r.price.region)) ||
108
+ Date.parse(r.price.effective_from) > Date.parse(r.effective_at) ||
109
+ (r.price.effective_to !== null && Date.parse(r.price.effective_to) <= Date.parse(r.effective_at))
110
+ )
111
+ return false;
112
+ if (action === "cost" || action === "calculate") {
113
+ if (r.status === "unpriced") return r.cost === null && r.components === null;
114
+ if (
115
+ !decimal(r.cost) ||
116
+ !object(r.components) ||
117
+ Object.keys(r.components).sort().join() !== "cache_read,cache_write,input,output,request" ||
118
+ !Object.values(r.components).every(decimal) ||
119
+ Object.values(r.components).reduce((n, v) => n + units(v), 0n) !== units(r.cost)
120
+ )
121
+ return false;
122
+ if (["input_tokens", "output_tokens"].some((k) => !Object.hasOwn(q, k) && !r.missing_dimensions.includes(k)))
123
+ return false;
124
+ }
125
+ return true;
126
+ }
127
+ export function pricingPayload(o) {
128
+ const q = o.query;
129
+ const context = Object.fromEntries(
130
+ ["provider", "channel", "region"].filter((k) => q[k] !== undefined).map((k) => [k, q[k]]),
131
+ );
132
+ if (q.at) context.at = `${q.at}T00:00:00Z`;
133
+ if (q.batch) context.service_tier = "batch";
134
+ if (o.action === "lookup") return { model: q.model, context };
135
+ const usage = {};
136
+ const put = (key, value) => {
137
+ if (value !== undefined) usage[key] = value;
138
+ };
139
+ let trace;
140
+ if (o.traceFormat === "anthropic.messages") {
141
+ put("input_tokens", q.input_tokens);
142
+ put("output_tokens", q.output_tokens);
143
+ put("cache_read_input_tokens", q.cache_read_tokens);
144
+ put("cache_creation_input_tokens", q.cache_write_tokens);
145
+ if (q.cache_write_5m_tokens !== undefined || q.cache_write_1h_tokens !== undefined)
146
+ usage.cache_creation = {
147
+ ...(q.cache_write_5m_tokens === undefined ? {} : { ephemeral_5m_input_tokens: q.cache_write_5m_tokens }),
148
+ ...(q.cache_write_1h_tokens === undefined ? {} : { ephemeral_1h_input_tokens: q.cache_write_1h_tokens }),
149
+ };
150
+ trace = { model: q.model, usage };
151
+ } else if (o.traceFormat.startsWith("openai.")) {
152
+ const chat = o.traceFormat === "openai.chat_completions";
153
+ put(chat ? "prompt_tokens" : "input_tokens", q.input_tokens);
154
+ put(chat ? "completion_tokens" : "output_tokens", q.output_tokens);
155
+ if (q.cache_read_tokens !== undefined)
156
+ usage[chat ? "prompt_tokens_details" : "input_tokens_details"] = { cached_tokens: q.cache_read_tokens };
157
+ trace = { model: q.model, usage };
158
+ } else {
159
+ usage["gen_ai.request.model"] = q.model;
160
+ put("gen_ai.usage.input_tokens", q.input_tokens);
161
+ put("gen_ai.usage.output_tokens", q.output_tokens);
162
+ put("gen_ai.usage.cache_read.input_tokens", q.cache_read_tokens);
163
+ trace = { attributes: usage };
164
+ }
165
+ return { format: o.traceFormat, trace, context };
166
+ }
167
+ export async function runPricing(o) {
168
+ if (o.api === "reference") return runReferencePricing(o);
169
+ if (o.action === "history")
170
+ return fail(
171
+ "unsupported",
172
+ "pricing_history_unavailable",
173
+ "The public API documents no history endpoint. Historical charts remain available with --api reference and a loopback --url; no public history request was made.",
174
+ );
175
+ if (o.action === "market") return runPublicMarket(o);
176
+ const path =
177
+ o.action === "models"
178
+ ? "/v1/pricing/models"
179
+ : o.action === "lookup"
180
+ ? "/v1/pricing/lookup"
181
+ : "/v1/pricing/calculate";
182
+ const res = await send(o.origin, path, {
183
+ method: o.action === "models" ? "GET" : "POST",
184
+ json: o.action === "models" ? null : pricingPayload(o),
185
+ maxBytes: 1024 * 1024,
186
+ signal: AbortSignal.timeout(o.timeoutMs),
187
+ useGlobalAgent: true,
188
+ });
189
+ const failure = responseFailure(res);
190
+ if (failure) return failure;
191
+ const d = parseJsonObject(res);
192
+ if (o.action === "models") {
193
+ if (!validateModels(d)) return fail("verification_failed", "pricing_contract_mismatch");
194
+ const q = o.query;
195
+ const matches = d.models.filter(
196
+ (m) =>
197
+ (!q.model || m.model_id === q.model || m.aliases.some((a) => a.model === q.model)) &&
198
+ (!q.provider || m.aliases.some((a) => a.provider === q.provider && (!q.channel || a.channel === q.channel))) &&
199
+ (!q.channel || m.prices.some((p) => p.channel === q.channel && (!q.region || p.region === q.region))) &&
200
+ (!q.region || m.prices.some((p) => p.region === q.region && (!q.channel || p.channel === q.channel))),
201
+ );
202
+ return {
203
+ outcome: "ok",
204
+ data: {
205
+ api: "public",
206
+ action: o.action,
207
+ request: q,
208
+ catalog: d.catalog,
209
+ as_of: d.as_of,
210
+ formats: d.formats,
211
+ providers: d.providers,
212
+ channels: d.channels,
213
+ regions: d.regions,
214
+ service_tiers: d.service_tiers,
215
+ result: {
216
+ models: matches.slice(0, q.limit),
217
+ matched: matches.length,
218
+ total_models: d.models.length,
219
+ truncated: matches.length > q.limit,
220
+ },
221
+ },
222
+ };
223
+ }
224
+ if (!validatePublicResult(d, o.action, o.query)) return fail("verification_failed", "pricing_contract_mismatch");
225
+ const r = d.results[0];
226
+ return {
227
+ outcome: "ok",
228
+ data: {
229
+ api: "public",
230
+ action: o.action,
231
+ request: o.query,
232
+ ...(o.action === "lookup" ? {} : { trace_format: o.traceFormat }),
233
+ catalog: null,
234
+ result: r,
235
+ },
236
+ };
237
+ }
238
+
239
+ function responseFailure(res) {
240
+ if (res.kind === "error") return fail("connection_failed", res.reason);
241
+ if (res.status >= 300 && res.status < 400) return fail("redirect_rejected", "pricing_redirect_rejected");
242
+ if (res.status === 429) return fail("rate_limited", "pricing_rate_limited");
243
+ if (res.status >= 500) return fail("unhealthy", "pricing_service_unhealthy");
244
+ if ([401, 403, 404, 405].includes(res.status)) return fail("unsupported", "public_pricing_unavailable");
245
+ if (res.tooLarge) return fail("unsupported", "pricing_response_too_large");
246
+ if ([400, 422].includes(res.status)) return fail("invalid_input", "pricing_request_rejected");
247
+ if (res.status !== 200) return fail("unsupported", "pricing_response_unsupported");
248
+ return null;
249
+ }
250
+
251
+ async function runPublicMarket(o) {
252
+ if (o.query.at)
253
+ return fail(
254
+ "unsupported",
255
+ "market_date_unsupported",
256
+ "The public market board uses current discovery and lookups. Use lookup --at for one historical price; historical board coverage is unavailable.",
257
+ );
258
+ const signal = AbortSignal.timeout(o.timeoutMs);
259
+ const options = { signal, maxBytes: 1024 * 1024, useGlobalAgent: true };
260
+ const discovery = await send(o.origin, "/v1/pricing/models", options);
261
+ const error = responseFailure(discovery);
262
+ if (error) return error;
263
+ const d = parseJsonObject(discovery);
264
+ if (!validateModels(d)) return fail("verification_failed", "pricing_contract_mismatch");
265
+ const q = o.query;
266
+ const matches = d.models.filter((m) => m.prices.some((p) => p.channel === q.channel && p.region === q.region));
267
+ const selected = matches.slice(0, q.limit),
268
+ items = new Array(selected.length);
269
+ let next = 0,
270
+ failure = null;
271
+ const worker = async () => {
272
+ while (!failure && next < selected.length) {
273
+ const index = next++,
274
+ model = selected[index].model_id;
275
+ const request = { model, channel: q.channel, region: q.region };
276
+ const res = await send(o.origin, "/v1/pricing/lookup", {
277
+ ...options,
278
+ method: "POST",
279
+ json: { model, context: { channel: q.channel, region: q.region } },
280
+ });
281
+ const error = responseFailure(res);
282
+ if (error) {
283
+ failure = error;
284
+ break;
285
+ }
286
+ const result = parseJsonObject(res);
287
+ if (!validatePublicResult(result, "lookup", request)) {
288
+ failure = fail("verification_failed", "pricing_contract_mismatch");
289
+ break;
290
+ }
291
+ items[index] = { requested_model: model, ...result.results[0] };
292
+ }
293
+ };
294
+ await Promise.all(Array.from({ length: Math.min(3, selected.length) }, worker));
295
+ if (failure) return failure;
296
+ return {
297
+ outcome: "ok",
298
+ data: {
299
+ api: "public",
300
+ action: "market",
301
+ request: q,
302
+ catalog: d.catalog,
303
+ as_of: d.as_of,
304
+ result: { items, matched: matches.length, truncated: matches.length > items.length, history_available: false },
305
+ },
306
+ };
307
+ }