@graphorin/pricing 0.6.0 → 0.7.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @graphorin/pricing
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#154](https://github.com/o-stepper/graphorin/pull/154) [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04) Thanks [@o-stepper](https://github.com/o-stepper)! - W-072: every export map's `import` condition becomes `default`, and the Node floor rises to `>=22.12.0`.
8
+
9
+ CJS consumers previously hit a bewildering `ERR_PACKAGE_PATH_NOT_EXPORTED` instead of a clear ESM-only signal. With the `default` condition, plain `require('@graphorin/core')` works via Node's stable `require(esm)` - which shipped in 22.12, hence the engines bump across every workspace manifest (packages, examples, benchmarks, docs; enforced by the widened mvp-readiness sweep). No dual-instance hazard: there is no CJS build, `require()` returns the same ESM module instance. ESM consumers are unaffected (`default` serves both paths; `types` stays first). The pack gate now runs attw under the full `node16` profile (was `esm-only`) and adds a runtime `require(esm)` smoke against the packed tarballs. Installs on Node 22.0-22.11 with `engine-strict` will refuse - upgrade Node (see the migration guide).
10
+
11
+ - [#160](https://github.com/o-stepper/graphorin/pull/160) [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156) Thanks [@o-stepper](https://github.com/o-stepper)! - W-097: `graphorin pricing refresh` finally works against the dataset the docs point at. New pure converter (`convertGenaiPrices` / `isGenaiPricesShape`, exported) maps the published `@pydantic/genai-prices` dataset (providers[] -> models[] -> per-Mtok prices incl. cache read/write legs) into per-token `ModelPrice` entries; unrepresentable model entries (tiered/conditional pricing) are skipped with a counter instead of failing the refresh, and the supported subset is pinned by a vendored fixture + doc comment. `refreshPricing` gains `format: 'auto' | 'graphorin' | 'genai-prices'` (default auto: native first, then detection), stamps converted snapshots `version: 'genai-prices+converted'` with an additive `PricingSnapshot.conversion { format, skipped }`, and REJECTS a dataset declaring a non-USD currency instead of silently stamping dollars. The CLI adds `--format` and prints the skipped count; the pricing reference documents a working raw-URL example. Native-shape refreshes are byte-identical; no baked-in endpoint was added (privacy contract).
12
+
13
+ ### Patch Changes
14
+
15
+ - [#160](https://github.com/o-stepper/graphorin/pull/160) [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156) Thanks [@o-stepper](https://github.com/o-stepper)! - W-045: the `Cost.amount` units contract is now consistent across the ecosystem - and it is WHOLE currency units (for USD: dollars, fractional values expected), never "smallest fractional unit" / cents as the core TSDoc previously claimed. The canonical producer `calculateCost` (@graphorin/pricing), `CostTracker` snapshots (@graphorin/observability) and the memory consolidator's `costUsd` all already operated in dollars; a consumer that followed the old doc and divided by 100 was off by 100x. Docs-only for the code paths, with a numeric pin test (1M input tokens at $5/Mtok = exactly `5`) freezing the convention. If you implemented a minor-units conversion against the old wording, remove it.
16
+
17
+ - [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Tarballs now ship `src/` so the published `dist/**/*.d.ts.map` files actually work (W-136): the maps referenced `../src/*.ts` that the `files` whitelist excluded, so go-to-definition fell back into `.d.ts` and the shipped maps were dead weight. The pack gate gains a `map-integrity` leg: every source referenced by a shipped map must resolve inside the tarball (or be embedded via `sourcesContent`), with an anti-vacuous guard - a package whose tsdown config emits declaration maps must contain a non-zero number of `.d.ts.map` files, so a cache-restored dist that silently dropped maps fails the gate instead of passing vacuously. `mvp-readiness` now requires `src` in every publishable `files` array.
18
+
19
+ - [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Every published package now declares its tree-shaking contract via `sideEffects` (W-137): 18 packages audited to a pure module scope get `false`, the CLI declares its bin entry (`["./dist/bin/*"]`), and `@graphorin/security` gets an explicit `true` - its secrets subsystem registers built-in resolvers and the SecretValue caller-context provider at import time, so marking it pure would let bundlers drop those registrations. `mvp-readiness` now fails any publishable manifest without a declared `sideEffects`, closing the drift for future packages.
20
+
21
+ - Updated dependencies [[`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04)]:
22
+ - @graphorin/core@0.7.0
23
+
24
+ ## 0.6.1
25
+
26
+ ### Patch Changes
27
+
28
+ - [#142](https://github.com/o-stepper/graphorin/pull/142) [`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430) Thanks [@o-stepper](https://github.com/o-stepper)! - Version constants and version-bearing strings now derive from each package's manifest at build time (`VERSION = pkg.version`; writer ids, client/server info, OTLP framework attributes, build-info metrics interpolate it). No behavioral change at the current version: the rendered strings are byte-identical. A release bump no longer edits source; the new `check-version-consistency` gate fails any reintroduced hardcoded framework version.
29
+
30
+ - Updated dependencies [[`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430)]:
31
+ - @graphorin/core@0.6.1
32
+
3
33
  ## 0.6.0
4
34
 
5
35
  ### Minor Changes
package/README.md CHANGED
@@ -66,4 +66,4 @@ MIT © 2026 [Oleksiy Stepurenko](https://github.com/o-stepper).
66
66
 
67
67
  ---
68
68
 
69
- **Project Graphorin** · v0.6.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
69
+ **Project Graphorin** · v0.7.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
@@ -0,0 +1,28 @@
1
+ import { ModelPrice } from "./types.js";
2
+
3
+ //#region src/convert-genai-prices.d.ts
4
+
5
+ /** Result of {@link convertGenaiPrices}. @stable */
6
+ interface GenaiPricesConversion {
7
+ readonly entries: ReadonlyArray<ModelPrice>;
8
+ /** Model entries the supported subset could not represent. */
9
+ readonly skipped: number;
10
+ }
11
+ /**
12
+ * Cheap structural detector: does this body look like the
13
+ * genai-prices dataset (a `providers` array of objects carrying
14
+ * `models` arrays)?
15
+ *
16
+ * @stable
17
+ */
18
+ declare function isGenaiPricesShape(body: unknown): boolean;
19
+ /**
20
+ * Convert a genai-prices dataset body. Tolerant: unrepresentable model
21
+ * entries are counted in `skipped`, never thrown on.
22
+ *
23
+ * @stable
24
+ */
25
+ declare function convertGenaiPrices(body: unknown): GenaiPricesConversion;
26
+ //#endregion
27
+ export { GenaiPricesConversion, convertGenaiPrices, isGenaiPricesShape };
28
+ //# sourceMappingURL=convert-genai-prices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert-genai-prices.d.ts","names":[],"sources":["../src/convert-genai-prices.ts"],"sourcesContent":[],"mappings":";;;;;UAgDiB,qBAAA;oBACG,cAAc;;;;;;;;;;;iBAYlB,kBAAA;;;;;;;iBAeA,kBAAA,iBAAmC"}
@@ -0,0 +1,90 @@
1
+ //#region src/convert-genai-prices.ts
2
+ const MTOK = 1e6;
3
+ /**
4
+ * Cheap structural detector: does this body look like the
5
+ * genai-prices dataset (a `providers` array of objects carrying
6
+ * `models` arrays)?
7
+ *
8
+ * @stable
9
+ */
10
+ function isGenaiPricesShape(body) {
11
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return false;
12
+ const providers = body.providers;
13
+ if (!Array.isArray(providers) || providers.length === 0) return false;
14
+ return providers.every((p) => typeof p === "object" && p !== null && Array.isArray(p.models));
15
+ }
16
+ /**
17
+ * Convert a genai-prices dataset body. Tolerant: unrepresentable model
18
+ * entries are counted in `skipped`, never thrown on.
19
+ *
20
+ * @stable
21
+ */
22
+ function convertGenaiPrices(body) {
23
+ if (!isGenaiPricesShape(body)) throw new Error("convertGenaiPrices: body does not match the genai-prices dataset shape");
24
+ const providers = body.providers;
25
+ const entries = [];
26
+ let skipped = 0;
27
+ for (const rawProvider of providers) {
28
+ const provider = rawProvider;
29
+ const providerId = firstString(provider.id, provider.provider_id, provider.name);
30
+ if (providerId === void 0) {
31
+ skipped += provider.models.length;
32
+ continue;
33
+ }
34
+ for (const rawModel of provider.models) {
35
+ const entry = convertModel(providerId, rawModel);
36
+ if (entry === null) skipped += 1;
37
+ else entries.push(entry);
38
+ }
39
+ }
40
+ return Object.freeze({
41
+ entries: Object.freeze(entries),
42
+ skipped
43
+ });
44
+ }
45
+ function convertModel(providerId, rawModel) {
46
+ if (typeof rawModel !== "object" || rawModel === null) return null;
47
+ const model = rawModel;
48
+ const modelId = firstString(model.id, model.model, model.name);
49
+ if (modelId === void 0) return null;
50
+ const prices = selectPriceRecord(model.prices);
51
+ if (prices === null) return null;
52
+ const input = perToken(prices.input_mtok);
53
+ const output = perToken(prices.output_mtok);
54
+ if (input === void 0 || output === void 0) return null;
55
+ const cacheRead = perToken(prices.cache_read_mtok);
56
+ const cacheWrite = perToken(prices.cache_write_mtok);
57
+ return Object.freeze({
58
+ provider: providerId.toLowerCase(),
59
+ model: modelId.toLowerCase(),
60
+ inputUsdPerToken: input,
61
+ outputUsdPerToken: output,
62
+ ...cacheRead !== void 0 ? { cachedReadUsdPerToken: cacheRead } : {},
63
+ ...cacheWrite !== void 0 ? { cacheWriteUsdPerToken: cacheWrite } : {}
64
+ });
65
+ }
66
+ /**
67
+ * Pick the price record to convert. A plain object is used directly;
68
+ * an array is usable only when it holds exactly ONE record (multiple
69
+ * records mean conditional / time-tiered pricing the flat
70
+ * {@link ModelPrice} cannot express - skipped, per the module doc).
71
+ */
72
+ function selectPriceRecord(prices) {
73
+ if (Array.isArray(prices)) {
74
+ if (prices.length !== 1) return null;
75
+ return selectPriceRecord(prices[0]);
76
+ }
77
+ if (typeof prices === "object" && prices !== null) return prices;
78
+ return null;
79
+ }
80
+ /** Per-Mtok USD figure -> per-token USD; non-finite/tiered -> undefined. */
81
+ function perToken(mtok) {
82
+ return typeof mtok === "number" && Number.isFinite(mtok) && mtok >= 0 ? mtok / MTOK : void 0;
83
+ }
84
+ function firstString(...values) {
85
+ for (const value of values) if (typeof value === "string" && value.length > 0) return value;
86
+ }
87
+
88
+ //#endregion
89
+ export { convertGenaiPrices, isGenaiPricesShape };
90
+ //# sourceMappingURL=convert-genai-prices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convert-genai-prices.js","names":["entries: ModelPrice[]"],"sources":["../src/convert-genai-prices.ts"],"sourcesContent":["/**\n * W-097: pure converter from the `@pydantic/genai-prices` published\n * dataset into graphorin-native {@link ModelPrice} entries, so\n * `graphorin pricing refresh --url <genai-prices raw URL>` actually\n * works - previously the docs pointed operators at that dataset while\n * `refreshPricing` only accepted the native shape, leaving every\n * operator to write their own transformer.\n *\n * Supported subset (documented deliberately - the converter is\n * tolerant and SKIPS what it cannot represent, it never throws on a\n * strange model entry):\n *\n * ```jsonc\n * {\n * \"providers\": [\n * {\n * \"id\": \"anthropic\", // or \"provider_id\" / \"name\"\n * \"models\": [\n * {\n * \"id\": \"claude-sonnet-4-5\", // or \"model\" / \"name\"\n * \"prices\": {\n * \"input_mtok\": 3, // USD per million tokens\n * \"output_mtok\": 15,\n * \"cache_read_mtok\": 0.3, // optional\n * \"cache_write_mtok\": 3.75 // optional\n * }\n * }\n * ]\n * }\n * ]\n * }\n * ```\n *\n * Per-Mtok figures divide by 1e6 into per-token USD. Model entries\n * whose `prices` is an ARRAY (conditional / time-tiered pricing) are\n * used only when the array holds exactly one usable record; everything\n * else - tiered objects, missing input/output figures - lands in the\n * `skipped` counter instead of failing the refresh. The dataset is\n * dollar-denominated; the currency guard lives in `refreshPricing`.\n *\n * @packageDocumentation\n */\n\nimport type { ModelPrice } from './types.js';\n\nconst MTOK = 1_000_000;\n\n/** Result of {@link convertGenaiPrices}. @stable */\nexport interface GenaiPricesConversion {\n readonly entries: ReadonlyArray<ModelPrice>;\n /** Model entries the supported subset could not represent. */\n readonly skipped: number;\n}\n\n/**\n * Cheap structural detector: does this body look like the\n * genai-prices dataset (a `providers` array of objects carrying\n * `models` arrays)?\n *\n * @stable\n */\nexport function isGenaiPricesShape(body: unknown): boolean {\n if (body === null || typeof body !== 'object' || Array.isArray(body)) return false;\n const providers = (body as { providers?: unknown }).providers;\n if (!Array.isArray(providers) || providers.length === 0) return false;\n return providers.every(\n (p) => typeof p === 'object' && p !== null && Array.isArray((p as { models?: unknown }).models),\n );\n}\n\n/**\n * Convert a genai-prices dataset body. Tolerant: unrepresentable model\n * entries are counted in `skipped`, never thrown on.\n *\n * @stable\n */\nexport function convertGenaiPrices(body: unknown): GenaiPricesConversion {\n if (!isGenaiPricesShape(body)) {\n throw new Error('convertGenaiPrices: body does not match the genai-prices dataset shape');\n }\n const providers = (body as { providers: ReadonlyArray<unknown> }).providers;\n const entries: ModelPrice[] = [];\n let skipped = 0;\n for (const rawProvider of providers) {\n const provider = rawProvider as {\n id?: unknown;\n provider_id?: unknown;\n name?: unknown;\n models: ReadonlyArray<unknown>;\n };\n const providerId = firstString(provider.id, provider.provider_id, provider.name);\n if (providerId === undefined) {\n skipped += provider.models.length;\n continue;\n }\n for (const rawModel of provider.models) {\n const entry = convertModel(providerId, rawModel);\n if (entry === null) skipped += 1;\n else entries.push(entry);\n }\n }\n return Object.freeze({ entries: Object.freeze(entries), skipped });\n}\n\nfunction convertModel(providerId: string, rawModel: unknown): ModelPrice | null {\n if (typeof rawModel !== 'object' || rawModel === null) return null;\n const model = rawModel as {\n id?: unknown;\n model?: unknown;\n name?: unknown;\n prices?: unknown;\n };\n const modelId = firstString(model.id, model.model, model.name);\n if (modelId === undefined) return null;\n const prices = selectPriceRecord(model.prices);\n if (prices === null) return null;\n const input = perToken(prices.input_mtok);\n const output = perToken(prices.output_mtok);\n if (input === undefined || output === undefined) return null;\n const cacheRead = perToken(prices.cache_read_mtok);\n const cacheWrite = perToken(prices.cache_write_mtok);\n return Object.freeze({\n provider: providerId.toLowerCase(),\n model: modelId.toLowerCase(),\n inputUsdPerToken: input,\n outputUsdPerToken: output,\n ...(cacheRead !== undefined ? { cachedReadUsdPerToken: cacheRead } : {}),\n ...(cacheWrite !== undefined ? { cacheWriteUsdPerToken: cacheWrite } : {}),\n });\n}\n\ninterface GenaiPriceRecord {\n readonly input_mtok?: unknown;\n readonly output_mtok?: unknown;\n readonly cache_read_mtok?: unknown;\n readonly cache_write_mtok?: unknown;\n}\n\n/**\n * Pick the price record to convert. A plain object is used directly;\n * an array is usable only when it holds exactly ONE record (multiple\n * records mean conditional / time-tiered pricing the flat\n * {@link ModelPrice} cannot express - skipped, per the module doc).\n */\nfunction selectPriceRecord(prices: unknown): GenaiPriceRecord | null {\n if (Array.isArray(prices)) {\n if (prices.length !== 1) return null;\n return selectPriceRecord(prices[0]);\n }\n if (typeof prices === 'object' && prices !== null) return prices as GenaiPriceRecord;\n return null;\n}\n\n/** Per-Mtok USD figure -> per-token USD; non-finite/tiered -> undefined. */\nfunction perToken(mtok: unknown): number | undefined {\n return typeof mtok === 'number' && Number.isFinite(mtok) && mtok >= 0 ? mtok / MTOK : undefined;\n}\n\nfunction firstString(...values: ReadonlyArray<unknown>): string | undefined {\n for (const value of values) {\n if (typeof value === 'string' && value.length > 0) return value;\n }\n return undefined;\n}\n"],"mappings":";AA6CA,MAAM,OAAO;;;;;;;;AAgBb,SAAgB,mBAAmB,MAAwB;AACzD,KAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,KAAK,CAAE,QAAO;CAC7E,MAAM,YAAa,KAAiC;AACpD,KAAI,CAAC,MAAM,QAAQ,UAAU,IAAI,UAAU,WAAW,EAAG,QAAO;AAChE,QAAO,UAAU,OACd,MAAM,OAAO,MAAM,YAAY,MAAM,QAAQ,MAAM,QAAS,EAA2B,OAAO,CAChG;;;;;;;;AASH,SAAgB,mBAAmB,MAAsC;AACvE,KAAI,CAAC,mBAAmB,KAAK,CAC3B,OAAM,IAAI,MAAM,yEAAyE;CAE3F,MAAM,YAAa,KAA+C;CAClE,MAAMA,UAAwB,EAAE;CAChC,IAAI,UAAU;AACd,MAAK,MAAM,eAAe,WAAW;EACnC,MAAM,WAAW;EAMjB,MAAM,aAAa,YAAY,SAAS,IAAI,SAAS,aAAa,SAAS,KAAK;AAChF,MAAI,eAAe,QAAW;AAC5B,cAAW,SAAS,OAAO;AAC3B;;AAEF,OAAK,MAAM,YAAY,SAAS,QAAQ;GACtC,MAAM,QAAQ,aAAa,YAAY,SAAS;AAChD,OAAI,UAAU,KAAM,YAAW;OAC1B,SAAQ,KAAK,MAAM;;;AAG5B,QAAO,OAAO,OAAO;EAAE,SAAS,OAAO,OAAO,QAAQ;EAAE;EAAS,CAAC;;AAGpE,SAAS,aAAa,YAAoB,UAAsC;AAC9E,KAAI,OAAO,aAAa,YAAY,aAAa,KAAM,QAAO;CAC9D,MAAM,QAAQ;CAMd,MAAM,UAAU,YAAY,MAAM,IAAI,MAAM,OAAO,MAAM,KAAK;AAC9D,KAAI,YAAY,OAAW,QAAO;CAClC,MAAM,SAAS,kBAAkB,MAAM,OAAO;AAC9C,KAAI,WAAW,KAAM,QAAO;CAC5B,MAAM,QAAQ,SAAS,OAAO,WAAW;CACzC,MAAM,SAAS,SAAS,OAAO,YAAY;AAC3C,KAAI,UAAU,UAAa,WAAW,OAAW,QAAO;CACxD,MAAM,YAAY,SAAS,OAAO,gBAAgB;CAClD,MAAM,aAAa,SAAS,OAAO,iBAAiB;AACpD,QAAO,OAAO,OAAO;EACnB,UAAU,WAAW,aAAa;EAClC,OAAO,QAAQ,aAAa;EAC5B,kBAAkB;EAClB,mBAAmB;EACnB,GAAI,cAAc,SAAY,EAAE,uBAAuB,WAAW,GAAG,EAAE;EACvE,GAAI,eAAe,SAAY,EAAE,uBAAuB,YAAY,GAAG,EAAE;EAC1E,CAAC;;;;;;;;AAgBJ,SAAS,kBAAkB,QAA0C;AACnE,KAAI,MAAM,QAAQ,OAAO,EAAE;AACzB,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,kBAAkB,OAAO,GAAG;;AAErC,KAAI,OAAO,WAAW,YAAY,WAAW,KAAM,QAAO;AAC1D,QAAO;;;AAIT,SAAS,SAAS,MAAmC;AACnD,QAAO,OAAO,SAAS,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ,IAAI,OAAO,OAAO;;AAGxF,SAAS,YAAY,GAAG,QAAoD;AAC1E,MAAK,MAAM,SAAS,OAClB,KAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,QAAO"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_PRICING_AUTO_REFRESH, PricingAutoRefreshConfig, PricingConfig } from "./config.js";
2
2
  import { Cost, LookupPriceArgs, LookupPriceResult, ModelPrice, PricingDiffEntry, PricingSnapshot, PricingTraceSpanLike } from "./types.js";
3
+ import { GenaiPricesConversion, convertGenaiPrices, isGenaiPricesShape } from "./convert-genai-prices.js";
3
4
  import { diffPricing } from "./diff.js";
4
5
  import { _resetLookupWarningsForTesting, calculateCost, lookupPrice, setLookupWarnSink } from "./lookup.js";
5
6
  import { MissingModelEntry, listMissingModels } from "./missing.js";
@@ -34,8 +35,7 @@ import { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest } from "./snapsho
34
35
  *
35
36
  * @packageDocumentation
36
37
  */
37
- /** Canonical version constant. Mirrors the `package.json` version. */
38
- declare const VERSION = "0.6.0";
38
+ declare const VERSION: string;
39
39
  //#endregion
40
- export { BUNDLED_SNAPSHOT, type Cost, DEFAULT_PRICING_AUTO_REFRESH, type LookupPriceArgs, type LookupPriceResult, type MissingModelEntry, type ModelPrice, type PricingAutoRefreshConfig, type PricingConfig, type PricingDiffEntry, type PricingSnapshot, type PricingTraceSpanLike, type RefreshPricingOptions, SNAPSHOT_DATE, VERSION, _resetLookupWarningsForTesting, calculateCost, computeEntriesDigest, diffPricing, listMissingModels, lookupPrice, refreshPricing, setLookupWarnSink };
40
+ export { BUNDLED_SNAPSHOT, type Cost, DEFAULT_PRICING_AUTO_REFRESH, type GenaiPricesConversion, type LookupPriceArgs, type LookupPriceResult, type MissingModelEntry, type ModelPrice, type PricingAutoRefreshConfig, type PricingConfig, type PricingDiffEntry, type PricingSnapshot, type PricingTraceSpanLike, type RefreshPricingOptions, SNAPSHOT_DATE, VERSION, _resetLookupWarningsForTesting, calculateCost, computeEntriesDigest, convertGenaiPrices, diffPricing, isGenaiPricesShape, listMissingModels, lookupPrice, refreshPricing, setLookupWarnSink };
41
41
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;;AA4BA;;;;;;;;;;;;;;;;;;cAAa,OAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;;;;AA8BA;;;;;;;;;;;;;;;;cAAa"}
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { version } from "./package.js";
1
2
  import { DEFAULT_PRICING_AUTO_REFRESH } from "./config.js";
3
+ import { convertGenaiPrices, isGenaiPricesShape } from "./convert-genai-prices.js";
2
4
  import { diffPricing } from "./diff.js";
3
5
  import { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest } from "./snapshot/bundled.js";
4
6
  import { _resetLookupWarningsForTesting, calculateCost, lookupPrice, setLookupWarnSink } from "./lookup.js";
5
7
  import { listMissingModels } from "./missing.js";
6
8
  import { refreshPricing } from "./refresh.js";
7
- import "./snapshot/index.js";
8
9
 
9
10
  //#region src/index.ts
10
11
  /**
@@ -33,9 +34,9 @@ import "./snapshot/index.js";
33
34
  *
34
35
  * @packageDocumentation
35
36
  */
36
- /** Canonical version constant. Mirrors the `package.json` version. */
37
- const VERSION = "0.6.0";
37
+ /** Canonical version constant, derived from `package.json` at build time. */
38
+ const VERSION = version;
38
39
 
39
40
  //#endregion
40
- export { BUNDLED_SNAPSHOT, DEFAULT_PRICING_AUTO_REFRESH, SNAPSHOT_DATE, VERSION, _resetLookupWarningsForTesting, calculateCost, computeEntriesDigest, diffPricing, listMissingModels, lookupPrice, refreshPricing, setLookupWarnSink };
41
+ export { BUNDLED_SNAPSHOT, DEFAULT_PRICING_AUTO_REFRESH, SNAPSHOT_DATE, VERSION, _resetLookupWarningsForTesting, calculateCost, computeEntriesDigest, convertGenaiPrices, diffPricing, isGenaiPricesShape, listMissingModels, lookupPrice, refreshPricing, setLookupWarnSink };
41
42
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/pricing - bundled LLM pricing snapshot for the Graphorin\n * framework.\n *\n * Highlights:\n *\n * - {@link BUNDLED_SNAPSHOT} - bundled snapshot covering Anthropic,\n * OpenAI, Google, Mistral, Cohere + the local providers\n * (Ollama / llama.cpp). Each entry carries a per-token price and\n * the canonical SHA-256 digest of the `entries` array.\n * - {@link lookupPrice} - resolve a `(provider, model)` pair against a\n * snapshot. Returns `null` for unknown entries plus emits one WARN\n * per process-lifetime per unknown pair.\n * - {@link calculateCost} - multiply a price by token counts for a\n * single LLM call.\n * - {@link diffPricing} - row-by-row delta between two snapshots.\n * - {@link refreshPricing} - opt-in network call. Never automatic.\n * - {@link listMissingModels} - scan trace spans for unknown models;\n * used by `graphorin pricing missing` (Phase 15).\n *\n * The bundled snapshot is intentionally small - operators wanting the\n * full upstream catalogue should run `refreshPricing(...)` and\n * persist the result to disk.\n *\n * @packageDocumentation\n */\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.6.0';\n\nexport {\n DEFAULT_PRICING_AUTO_REFRESH,\n type PricingAutoRefreshConfig,\n type PricingConfig,\n} from './config.js';\nexport { diffPricing } from './diff.js';\nexport {\n _resetLookupWarningsForTesting,\n calculateCost,\n lookupPrice,\n setLookupWarnSink,\n} from './lookup.js';\nexport { listMissingModels, type MissingModelEntry } from './missing.js';\nexport { type RefreshPricingOptions, refreshPricing } from './refresh.js';\nexport { BUNDLED_SNAPSHOT, computeEntriesDigest, SNAPSHOT_DATE } from './snapshot/index.js';\nexport type {\n Cost,\n LookupPriceArgs,\n LookupPriceResult,\n ModelPrice,\n PricingDiffEntry,\n PricingSnapshot,\n PricingTraceSpanLike,\n} from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,UAAU"}
1
+ {"version":3,"file":"index.js","names":["VERSION: string","pkg.version"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/pricing - bundled LLM pricing snapshot for the Graphorin\n * framework.\n *\n * Highlights:\n *\n * - {@link BUNDLED_SNAPSHOT} - bundled snapshot covering Anthropic,\n * OpenAI, Google, Mistral, Cohere + the local providers\n * (Ollama / llama.cpp). Each entry carries a per-token price and\n * the canonical SHA-256 digest of the `entries` array.\n * - {@link lookupPrice} - resolve a `(provider, model)` pair against a\n * snapshot. Returns `null` for unknown entries plus emits one WARN\n * per process-lifetime per unknown pair.\n * - {@link calculateCost} - multiply a price by token counts for a\n * single LLM call.\n * - {@link diffPricing} - row-by-row delta between two snapshots.\n * - {@link refreshPricing} - opt-in network call. Never automatic.\n * - {@link listMissingModels} - scan trace spans for unknown models;\n * used by `graphorin pricing missing` (Phase 15).\n *\n * The bundled snapshot is intentionally small - operators wanting the\n * full upstream catalogue should run `refreshPricing(...)` and\n * persist the result to disk.\n *\n * @packageDocumentation\n */\n\n/** Canonical version constant, derived from `package.json` at build time. */\nimport pkg from '../package.json' with { type: 'json' };\n\nexport const VERSION: string = pkg.version;\n\nexport {\n DEFAULT_PRICING_AUTO_REFRESH,\n type PricingAutoRefreshConfig,\n type PricingConfig,\n} from './config.js';\nexport {\n convertGenaiPrices,\n type GenaiPricesConversion,\n isGenaiPricesShape,\n} from './convert-genai-prices.js';\nexport { diffPricing } from './diff.js';\nexport {\n _resetLookupWarningsForTesting,\n calculateCost,\n lookupPrice,\n setLookupWarnSink,\n} from './lookup.js';\nexport { listMissingModels, type MissingModelEntry } from './missing.js';\nexport { type RefreshPricingOptions, refreshPricing } from './refresh.js';\nexport { BUNDLED_SNAPSHOT, computeEntriesDigest, SNAPSHOT_DATE } from './snapshot/index.js';\nexport type {\n Cost,\n LookupPriceArgs,\n LookupPriceResult,\n ModelPrice,\n PricingDiffEntry,\n PricingSnapshot,\n PricingTraceSpanLike,\n} from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAaA,UAAkBC"}
package/dist/lookup.d.ts CHANGED
@@ -38,6 +38,11 @@ declare function lookupPrice(args: LookupPriceArgs, snapshot?: PricingSnapshot):
38
38
  * declares one, else at the full input rate (a cache write is at minimum a
39
39
  * normal input token - the fallback never under-bills relative to no cache).
40
40
  *
41
+ * Units contract (W-045): this is the CANONICAL producer of core
42
+ * `Cost.amount`, and it returns WHOLE US dollars (per-token USD rates
43
+ * times token counts) - never cents / minor units. One million input
44
+ * tokens at `inputUsdPerToken = 3e-6` cost `3` (three dollars).
45
+ *
41
46
  * @stable
42
47
  */
43
48
  declare function calculateCost(args: LookupPriceArgs & {
@@ -1 +1 @@
1
- {"version":3,"file":"lookup.d.ts","names":[],"sources":["../src/lookup.ts"],"sourcesContent":[],"mappings":";;;;;AA8HA;;;;;iBA1GgB,iBAAA;;;;iBASA,8BAAA,CAAA;;;;;;;;;;iBAaA,WAAA,OACR,4BACI,kBACT;;;;;;;;;;;;;;;;;;iBAiFa,aAAA,OACR;;;;;;;cAQI"}
1
+ {"version":3,"file":"lookup.d.ts","names":[],"sources":["../src/lookup.ts"],"sourcesContent":[],"mappings":";;;;;AAmIA;;;;;iBA/GgB,iBAAA;;;;iBASA,8BAAA,CAAA;;;;;;;;;;iBAaA,WAAA,OACR,4BACI,kBACT;;;;;;;;;;;;;;;;;;;;;;;iBAsFa,aAAA,OACR;;;;;;;cAQI"}
package/dist/lookup.js CHANGED
@@ -80,6 +80,11 @@ function warnOnce(args) {
80
80
  * declares one, else at the full input rate (a cache write is at minimum a
81
81
  * normal input token - the fallback never under-bills relative to no cache).
82
82
  *
83
+ * Units contract (W-045): this is the CANONICAL producer of core
84
+ * `Cost.amount`, and it returns WHOLE US dollars (per-token USD rates
85
+ * times token counts) - never cents / minor units. One million input
86
+ * tokens at `inputUsdPerToken = 3e-6` cost `3` (three dollars).
87
+ *
83
88
  * @stable
84
89
  */
85
90
  function calculateCost(args, snapshot = BUNDLED_SNAPSHOT) {
@@ -1 +1 @@
1
- {"version":3,"file":"lookup.js","names":["WARN_SINK: (line: string) => void"],"sources":["../src/lookup.ts"],"sourcesContent":["/**\n * `lookupPrice(...)` - resolve a per-token price for a given\n * (provider, model) pair against a snapshot. Returns `null` when the\n * model is unknown and emits one WARN per process-lifetime per\n * unknown (provider, model) pair.\n *\n * @packageDocumentation\n */\n\nimport { BUNDLED_SNAPSHOT } from './snapshot/bundled.js';\nimport type { LookupPriceArgs, LookupPriceResult, ModelPrice, PricingSnapshot } from './types.js';\n\nconst WARNED = new Set<string>();\n\n/**\n * Optional sink for the deduplicated WARN emitted on unknown models.\n * Defaults to `console.warn`. Override in tests.\n *\n * @internal\n */\nexport function setLookupWarnSink(sink: (line: string) => void): void {\n WARN_SINK = sink;\n}\n\nlet WARN_SINK: (line: string) => void = (line) => console.warn(line);\n\n/**\n * @internal - exposed for tests so the WARN-once cache can be reset.\n */\nexport function _resetLookupWarningsForTesting(): void {\n WARNED.clear();\n}\n\n/**\n * Resolve a per-token price for the (provider, model) pair. Returns\n * `null` when the snapshot does not contain an entry for the model.\n *\n * The function emits one WARN per process per unknown (provider, model)\n * pair so cost dashboards surface drift without spamming the log.\n *\n * @stable\n */\nexport function lookupPrice(\n args: LookupPriceArgs,\n snapshot: PricingSnapshot = BUNDLED_SNAPSHOT,\n): LookupPriceResult | null {\n const exact = snapshot.entries.find(\n (entry) =>\n entry.provider === args.provider &&\n entry.model === args.model &&\n (args.region === undefined || entry.region === undefined || entry.region === args.region),\n );\n\n if (exact !== undefined) return entryToResult(exact, snapshot);\n\n // Date-suffix fallback (core-provider-03): dated ids like\n // `claude-haiku-4-5-20251001` resolve to their dateless alias entry\n // (`claude-haiku-4-5`) so the snapshot does not need one row per\n // dated release.\n const dateless = args.model.replace(/-\\d{8}$/, '');\n if (dateless !== args.model) {\n const alias = snapshot.entries.find(\n (entry) =>\n entry.provider === args.provider &&\n entry.model === dateless &&\n (args.region === undefined || entry.region === undefined || entry.region === args.region),\n );\n if (alias !== undefined) return entryToResult(alias, snapshot);\n }\n\n // Wildcard fallback - used by local providers (`provider: 'ollama', model: '*'`).\n const wildcard = snapshot.entries.find(\n (entry) => entry.provider === args.provider && entry.model === '*',\n );\n if (wildcard !== undefined) return entryToResult(wildcard, snapshot);\n\n warnOnce(args);\n return null;\n}\n\nfunction entryToResult(entry: ModelPrice, snapshot: PricingSnapshot): LookupPriceResult {\n return {\n inputUsdPerToken: entry.inputUsdPerToken,\n outputUsdPerToken: entry.outputUsdPerToken,\n ...(entry.cachedReadUsdPerToken === undefined\n ? {}\n : { cachedReadUsdPerToken: entry.cachedReadUsdPerToken }),\n ...(entry.cacheWriteUsdPerToken === undefined\n ? {}\n : { cacheWriteUsdPerToken: entry.cacheWriteUsdPerToken }),\n ...(entry.reasoningUsdPerToken === undefined\n ? {}\n : { reasoningUsdPerToken: entry.reasoningUsdPerToken }),\n source: snapshot.source,\n snapshotDate: snapshot.snapshotDate,\n };\n}\n\nfunction warnOnce(args: LookupPriceArgs): void {\n const key = `${args.provider}/${args.model}`;\n if (WARNED.has(key)) return;\n WARNED.add(key);\n WARN_SINK(\n `[graphorin/pricing] WARN: no pricing entry for ${args.provider}/${args.model}; ` +\n 'cost is null. Consider running `graphorin pricing refresh` or contributing the ' +\n 'entry upstream.',\n );\n}\n\n/**\n * Multiply a per-token price by an integer token count. Returns `null`\n * when the price is unknown. Useful when caller wants to compute cost\n * for a single LLM call without instantiating the cost tracker.\n *\n * Token-count contract (PS-19):\n * - `inputTokens` **excludes** `cachedReadTokens` and `cacheWriteTokens` -\n * the cache legs are billed separately at their own rates, so pass the\n * non-cached prompt count to avoid double-billing.\n * - `reasoningTokens` are billed at `outputUsdPerToken` unless the model entry\n * declares an explicit `reasoningUsdPerToken`.\n * - `cacheWriteTokens` are billed at `cacheWriteUsdPerToken` when the entry\n * declares one, else at the full input rate (a cache write is at minimum a\n * normal input token - the fallback never under-bills relative to no cache).\n *\n * @stable\n */\nexport function calculateCost(\n args: LookupPriceArgs & {\n /** Non-cached prompt tokens (excludes cache reads AND cache writes). */\n readonly inputTokens: number;\n readonly outputTokens: number;\n readonly cachedReadTokens?: number;\n readonly cacheWriteTokens?: number;\n readonly reasoningTokens?: number;\n },\n snapshot: PricingSnapshot = BUNDLED_SNAPSHOT,\n): { readonly amount: number; readonly currency: 'USD' } | null {\n const price = lookupPrice(args, snapshot);\n if (price === null) return null;\n let amount = 0;\n // `inputTokens` is the NON-cached prompt; cached reads are billed separately\n // at their own (cheaper) rate, so they must be excluded from `inputTokens` to\n // avoid double-counting (PS-19).\n amount += price.inputUsdPerToken * args.inputTokens;\n amount += price.outputUsdPerToken * args.outputTokens;\n if (args.cachedReadTokens !== undefined && price.cachedReadUsdPerToken !== undefined) {\n amount += price.cachedReadUsdPerToken * args.cachedReadTokens;\n }\n if (args.cacheWriteTokens !== undefined) {\n amount += (price.cacheWriteUsdPerToken ?? price.inputUsdPerToken) * args.cacheWriteTokens;\n }\n if (args.reasoningTokens !== undefined) {\n // PS-19: reasoning tokens follow completion (output) pricing unless the\n // entry declares an explicit `reasoningUsdPerToken` - the documented\n // contract that was previously billed at $0 for every bundled entry.\n amount += (price.reasoningUsdPerToken ?? price.outputUsdPerToken) * args.reasoningTokens;\n }\n return { amount, currency: 'USD' };\n}\n"],"mappings":";;;;;;;;;;;AAYA,MAAM,yBAAS,IAAI,KAAa;;;;;;;AAQhC,SAAgB,kBAAkB,MAAoC;AACpE,aAAY;;AAGd,IAAIA,aAAqC,SAAS,QAAQ,KAAK,KAAK;;;;AAKpE,SAAgB,iCAAuC;AACrD,QAAO,OAAO;;;;;;;;;;;AAYhB,SAAgB,YACd,MACA,WAA4B,kBACF;CAC1B,MAAM,QAAQ,SAAS,QAAQ,MAC5B,UACC,MAAM,aAAa,KAAK,YACxB,MAAM,UAAU,KAAK,UACpB,KAAK,WAAW,UAAa,MAAM,WAAW,UAAa,MAAM,WAAW,KAAK,QACrF;AAED,KAAI,UAAU,OAAW,QAAO,cAAc,OAAO,SAAS;CAM9D,MAAM,WAAW,KAAK,MAAM,QAAQ,WAAW,GAAG;AAClD,KAAI,aAAa,KAAK,OAAO;EAC3B,MAAM,QAAQ,SAAS,QAAQ,MAC5B,UACC,MAAM,aAAa,KAAK,YACxB,MAAM,UAAU,aACf,KAAK,WAAW,UAAa,MAAM,WAAW,UAAa,MAAM,WAAW,KAAK,QACrF;AACD,MAAI,UAAU,OAAW,QAAO,cAAc,OAAO,SAAS;;CAIhE,MAAM,WAAW,SAAS,QAAQ,MAC/B,UAAU,MAAM,aAAa,KAAK,YAAY,MAAM,UAAU,IAChE;AACD,KAAI,aAAa,OAAW,QAAO,cAAc,UAAU,SAAS;AAEpE,UAAS,KAAK;AACd,QAAO;;AAGT,SAAS,cAAc,OAAmB,UAA8C;AACtF,QAAO;EACL,kBAAkB,MAAM;EACxB,mBAAmB,MAAM;EACzB,GAAI,MAAM,0BAA0B,SAChC,EAAE,GACF,EAAE,uBAAuB,MAAM,uBAAuB;EAC1D,GAAI,MAAM,0BAA0B,SAChC,EAAE,GACF,EAAE,uBAAuB,MAAM,uBAAuB;EAC1D,GAAI,MAAM,yBAAyB,SAC/B,EAAE,GACF,EAAE,sBAAsB,MAAM,sBAAsB;EACxD,QAAQ,SAAS;EACjB,cAAc,SAAS;EACxB;;AAGH,SAAS,SAAS,MAA6B;CAC7C,MAAM,MAAM,GAAG,KAAK,SAAS,GAAG,KAAK;AACrC,KAAI,OAAO,IAAI,IAAI,CAAE;AACrB,QAAO,IAAI,IAAI;AACf,WACE,kDAAkD,KAAK,SAAS,GAAG,KAAK,MAAM,oGAG/E;;;;;;;;;;;;;;;;;;;AAoBH,SAAgB,cACd,MAQA,WAA4B,kBACkC;CAC9D,MAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,KAAI,UAAU,KAAM,QAAO;CAC3B,IAAI,SAAS;AAIb,WAAU,MAAM,mBAAmB,KAAK;AACxC,WAAU,MAAM,oBAAoB,KAAK;AACzC,KAAI,KAAK,qBAAqB,UAAa,MAAM,0BAA0B,OACzE,WAAU,MAAM,wBAAwB,KAAK;AAE/C,KAAI,KAAK,qBAAqB,OAC5B,YAAW,MAAM,yBAAyB,MAAM,oBAAoB,KAAK;AAE3E,KAAI,KAAK,oBAAoB,OAI3B,YAAW,MAAM,wBAAwB,MAAM,qBAAqB,KAAK;AAE3E,QAAO;EAAE;EAAQ,UAAU;EAAO"}
1
+ {"version":3,"file":"lookup.js","names":["WARN_SINK: (line: string) => void"],"sources":["../src/lookup.ts"],"sourcesContent":["/**\n * `lookupPrice(...)` - resolve a per-token price for a given\n * (provider, model) pair against a snapshot. Returns `null` when the\n * model is unknown and emits one WARN per process-lifetime per\n * unknown (provider, model) pair.\n *\n * @packageDocumentation\n */\n\nimport { BUNDLED_SNAPSHOT } from './snapshot/bundled.js';\nimport type { LookupPriceArgs, LookupPriceResult, ModelPrice, PricingSnapshot } from './types.js';\n\nconst WARNED = new Set<string>();\n\n/**\n * Optional sink for the deduplicated WARN emitted on unknown models.\n * Defaults to `console.warn`. Override in tests.\n *\n * @internal\n */\nexport function setLookupWarnSink(sink: (line: string) => void): void {\n WARN_SINK = sink;\n}\n\nlet WARN_SINK: (line: string) => void = (line) => console.warn(line);\n\n/**\n * @internal - exposed for tests so the WARN-once cache can be reset.\n */\nexport function _resetLookupWarningsForTesting(): void {\n WARNED.clear();\n}\n\n/**\n * Resolve a per-token price for the (provider, model) pair. Returns\n * `null` when the snapshot does not contain an entry for the model.\n *\n * The function emits one WARN per process per unknown (provider, model)\n * pair so cost dashboards surface drift without spamming the log.\n *\n * @stable\n */\nexport function lookupPrice(\n args: LookupPriceArgs,\n snapshot: PricingSnapshot = BUNDLED_SNAPSHOT,\n): LookupPriceResult | null {\n const exact = snapshot.entries.find(\n (entry) =>\n entry.provider === args.provider &&\n entry.model === args.model &&\n (args.region === undefined || entry.region === undefined || entry.region === args.region),\n );\n\n if (exact !== undefined) return entryToResult(exact, snapshot);\n\n // Date-suffix fallback (core-provider-03): dated ids like\n // `claude-haiku-4-5-20251001` resolve to their dateless alias entry\n // (`claude-haiku-4-5`) so the snapshot does not need one row per\n // dated release.\n const dateless = args.model.replace(/-\\d{8}$/, '');\n if (dateless !== args.model) {\n const alias = snapshot.entries.find(\n (entry) =>\n entry.provider === args.provider &&\n entry.model === dateless &&\n (args.region === undefined || entry.region === undefined || entry.region === args.region),\n );\n if (alias !== undefined) return entryToResult(alias, snapshot);\n }\n\n // Wildcard fallback - used by local providers (`provider: 'ollama', model: '*'`).\n const wildcard = snapshot.entries.find(\n (entry) => entry.provider === args.provider && entry.model === '*',\n );\n if (wildcard !== undefined) return entryToResult(wildcard, snapshot);\n\n warnOnce(args);\n return null;\n}\n\nfunction entryToResult(entry: ModelPrice, snapshot: PricingSnapshot): LookupPriceResult {\n return {\n inputUsdPerToken: entry.inputUsdPerToken,\n outputUsdPerToken: entry.outputUsdPerToken,\n ...(entry.cachedReadUsdPerToken === undefined\n ? {}\n : { cachedReadUsdPerToken: entry.cachedReadUsdPerToken }),\n ...(entry.cacheWriteUsdPerToken === undefined\n ? {}\n : { cacheWriteUsdPerToken: entry.cacheWriteUsdPerToken }),\n ...(entry.reasoningUsdPerToken === undefined\n ? {}\n : { reasoningUsdPerToken: entry.reasoningUsdPerToken }),\n source: snapshot.source,\n snapshotDate: snapshot.snapshotDate,\n };\n}\n\nfunction warnOnce(args: LookupPriceArgs): void {\n const key = `${args.provider}/${args.model}`;\n if (WARNED.has(key)) return;\n WARNED.add(key);\n WARN_SINK(\n `[graphorin/pricing] WARN: no pricing entry for ${args.provider}/${args.model}; ` +\n 'cost is null. Consider running `graphorin pricing refresh` or contributing the ' +\n 'entry upstream.',\n );\n}\n\n/**\n * Multiply a per-token price by an integer token count. Returns `null`\n * when the price is unknown. Useful when caller wants to compute cost\n * for a single LLM call without instantiating the cost tracker.\n *\n * Token-count contract (PS-19):\n * - `inputTokens` **excludes** `cachedReadTokens` and `cacheWriteTokens` -\n * the cache legs are billed separately at their own rates, so pass the\n * non-cached prompt count to avoid double-billing.\n * - `reasoningTokens` are billed at `outputUsdPerToken` unless the model entry\n * declares an explicit `reasoningUsdPerToken`.\n * - `cacheWriteTokens` are billed at `cacheWriteUsdPerToken` when the entry\n * declares one, else at the full input rate (a cache write is at minimum a\n * normal input token - the fallback never under-bills relative to no cache).\n *\n * Units contract (W-045): this is the CANONICAL producer of core\n * `Cost.amount`, and it returns WHOLE US dollars (per-token USD rates\n * times token counts) - never cents / minor units. One million input\n * tokens at `inputUsdPerToken = 3e-6` cost `3` (three dollars).\n *\n * @stable\n */\nexport function calculateCost(\n args: LookupPriceArgs & {\n /** Non-cached prompt tokens (excludes cache reads AND cache writes). */\n readonly inputTokens: number;\n readonly outputTokens: number;\n readonly cachedReadTokens?: number;\n readonly cacheWriteTokens?: number;\n readonly reasoningTokens?: number;\n },\n snapshot: PricingSnapshot = BUNDLED_SNAPSHOT,\n): { readonly amount: number; readonly currency: 'USD' } | null {\n const price = lookupPrice(args, snapshot);\n if (price === null) return null;\n let amount = 0;\n // `inputTokens` is the NON-cached prompt; cached reads are billed separately\n // at their own (cheaper) rate, so they must be excluded from `inputTokens` to\n // avoid double-counting (PS-19).\n amount += price.inputUsdPerToken * args.inputTokens;\n amount += price.outputUsdPerToken * args.outputTokens;\n if (args.cachedReadTokens !== undefined && price.cachedReadUsdPerToken !== undefined) {\n amount += price.cachedReadUsdPerToken * args.cachedReadTokens;\n }\n if (args.cacheWriteTokens !== undefined) {\n amount += (price.cacheWriteUsdPerToken ?? price.inputUsdPerToken) * args.cacheWriteTokens;\n }\n if (args.reasoningTokens !== undefined) {\n // PS-19: reasoning tokens follow completion (output) pricing unless the\n // entry declares an explicit `reasoningUsdPerToken` - the documented\n // contract that was previously billed at $0 for every bundled entry.\n amount += (price.reasoningUsdPerToken ?? price.outputUsdPerToken) * args.reasoningTokens;\n }\n return { amount, currency: 'USD' };\n}\n"],"mappings":";;;;;;;;;;;AAYA,MAAM,yBAAS,IAAI,KAAa;;;;;;;AAQhC,SAAgB,kBAAkB,MAAoC;AACpE,aAAY;;AAGd,IAAIA,aAAqC,SAAS,QAAQ,KAAK,KAAK;;;;AAKpE,SAAgB,iCAAuC;AACrD,QAAO,OAAO;;;;;;;;;;;AAYhB,SAAgB,YACd,MACA,WAA4B,kBACF;CAC1B,MAAM,QAAQ,SAAS,QAAQ,MAC5B,UACC,MAAM,aAAa,KAAK,YACxB,MAAM,UAAU,KAAK,UACpB,KAAK,WAAW,UAAa,MAAM,WAAW,UAAa,MAAM,WAAW,KAAK,QACrF;AAED,KAAI,UAAU,OAAW,QAAO,cAAc,OAAO,SAAS;CAM9D,MAAM,WAAW,KAAK,MAAM,QAAQ,WAAW,GAAG;AAClD,KAAI,aAAa,KAAK,OAAO;EAC3B,MAAM,QAAQ,SAAS,QAAQ,MAC5B,UACC,MAAM,aAAa,KAAK,YACxB,MAAM,UAAU,aACf,KAAK,WAAW,UAAa,MAAM,WAAW,UAAa,MAAM,WAAW,KAAK,QACrF;AACD,MAAI,UAAU,OAAW,QAAO,cAAc,OAAO,SAAS;;CAIhE,MAAM,WAAW,SAAS,QAAQ,MAC/B,UAAU,MAAM,aAAa,KAAK,YAAY,MAAM,UAAU,IAChE;AACD,KAAI,aAAa,OAAW,QAAO,cAAc,UAAU,SAAS;AAEpE,UAAS,KAAK;AACd,QAAO;;AAGT,SAAS,cAAc,OAAmB,UAA8C;AACtF,QAAO;EACL,kBAAkB,MAAM;EACxB,mBAAmB,MAAM;EACzB,GAAI,MAAM,0BAA0B,SAChC,EAAE,GACF,EAAE,uBAAuB,MAAM,uBAAuB;EAC1D,GAAI,MAAM,0BAA0B,SAChC,EAAE,GACF,EAAE,uBAAuB,MAAM,uBAAuB;EAC1D,GAAI,MAAM,yBAAyB,SAC/B,EAAE,GACF,EAAE,sBAAsB,MAAM,sBAAsB;EACxD,QAAQ,SAAS;EACjB,cAAc,SAAS;EACxB;;AAGH,SAAS,SAAS,MAA6B;CAC7C,MAAM,MAAM,GAAG,KAAK,SAAS,GAAG,KAAK;AACrC,KAAI,OAAO,IAAI,IAAI,CAAE;AACrB,QAAO,IAAI,IAAI;AACf,WACE,kDAAkD,KAAK,SAAS,GAAG,KAAK,MAAM,oGAG/E;;;;;;;;;;;;;;;;;;;;;;;;AAyBH,SAAgB,cACd,MAQA,WAA4B,kBACkC;CAC9D,MAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,KAAI,UAAU,KAAM,QAAO;CAC3B,IAAI,SAAS;AAIb,WAAU,MAAM,mBAAmB,KAAK;AACxC,WAAU,MAAM,oBAAoB,KAAK;AACzC,KAAI,KAAK,qBAAqB,UAAa,MAAM,0BAA0B,OACzE,WAAU,MAAM,wBAAwB,KAAK;AAE/C,KAAI,KAAK,qBAAqB,OAC5B,YAAW,MAAM,yBAAyB,MAAM,oBAAoB,KAAK;AAE3E,KAAI,KAAK,oBAAoB,OAI3B,YAAW,MAAM,wBAAwB,MAAM,qBAAqB,KAAK;AAE3E,QAAO;EAAE;EAAQ,UAAU;EAAO"}
@@ -0,0 +1,6 @@
1
+ //#region package.json
2
+ var version = "0.7.0";
3
+
4
+ //#endregion
5
+ export { version };
6
+ //# sourceMappingURL=package.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/pricing\",\n \"version\": \"0.7.0\",\n \"description\": \"Bundled LLM pricing snapshot for the Graphorin framework. Ships per-provider, per-model input / output / cached-read prices alongside provenance metadata, a `lookupPrice(...)` resolver, snapshot diffing utilities, an opt-in `refreshPricing(...)` hook (network gated; never automatic), and `listMissingModels(...)` for trace audits.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/pricing\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/pricing\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"pricing\",\n \"tokens\",\n \"cost\",\n \"llm\"\n ],\n \"type\": \"module\",\n \"sideEffects\": false,\n \"engines\": {\n \"node\": \">=22.12.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"./snapshot\": {\n \"types\": \"./dist/snapshot/index.d.ts\",\n \"default\": \"./dist/snapshot/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"src\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\"\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {}\n}\n"],"mappings":";cAEa"}
package/dist/refresh.d.ts CHANGED
@@ -30,6 +30,13 @@ interface RefreshPricingOptions {
30
30
  readonly timeoutMs?: number;
31
31
  /** Caller-supplied abort signal, combined with the timeout. */
32
32
  readonly signal?: AbortSignal;
33
+ /**
34
+ * W-097: accepted body format. `'auto'` (default) tries the native
35
+ * graphorin shape, then auto-detects + converts the
36
+ * `@pydantic/genai-prices` dataset; the explicit values pin one
37
+ * format and fail fast on anything else.
38
+ */
39
+ readonly format?: 'auto' | 'graphorin' | 'genai-prices';
33
40
  }
34
41
  /**
35
42
  * Pull a fresh snapshot from the supplied URL and return it. Network
@@ -1 +1 @@
1
- {"version":3,"file":"refresh.d.ts","names":[],"sources":["../src/refresh.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;UAwBiB,qBAAA;;;;qBAII,SAAS;;8BAEA;;;;;;;;;;;;;;oBAcV;;;;;;;;;iBAUE,cAAA,OAAqB,wBAAwB,QAAQ"}
1
+ {"version":3,"file":"refresh.d.ts","names":[],"sources":["../src/refresh.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;UAyBiB,qBAAA;;;;qBAII,SAAS;;8BAEA;;;;;;;;;;;;;;oBAcV;;;;;;;;;;;;;;;;iBAiBE,cAAA,OAAqB,wBAAwB,QAAQ"}
package/dist/refresh.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { convertGenaiPrices, isGenaiPricesShape } from "./convert-genai-prices.js";
1
2
  import { computeEntriesDigest } from "./snapshot/bundled.js";
2
3
 
3
4
  //#region src/refresh.ts
@@ -38,18 +39,49 @@ async function refreshPricing(opts) {
38
39
  throw err;
39
40
  }
40
41
  if (!res.ok) throw new Error(`refreshPricing: HTTP ${res.status} ${res.statusText} from ${opts.url}`);
41
- const entries = parseEntries(await res.json());
42
+ const body = await res.json();
43
+ const format = opts.format ?? "auto";
44
+ let entries;
45
+ let conversion;
46
+ if (format === "graphorin") entries = parseEntries(body);
47
+ else if (format === "genai-prices") ({entries, conversion} = convertForeign(body, opts.url));
48
+ else try {
49
+ entries = parseEntries(body);
50
+ } catch (nativeError) {
51
+ if (!isGenaiPricesShape(body)) throw nativeError;
52
+ ({entries, conversion} = convertForeign(body, opts.url));
53
+ }
42
54
  const snapshotDate = opts.snapshotDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
43
- const version = opts.version ?? "graphorin/0.1+refreshed";
55
+ const version = opts.version ?? (conversion !== void 0 ? "genai-prices+converted" : "graphorin/0.1+refreshed");
44
56
  return Object.freeze({
45
57
  version,
46
58
  source: opts.url,
47
59
  snapshotDate,
48
60
  currency: "USD",
49
61
  sha256: computeEntriesDigest(entries),
50
- entries
62
+ entries,
63
+ ...conversion !== void 0 ? { conversion } : {}
51
64
  });
52
65
  }
66
+ /**
67
+ * W-097: convert a genai-prices dataset body, guarding the currency -
68
+ * `PricingSnapshot.currency` is a `'USD'` literal, so a body that
69
+ * DECLARES another currency must fail loudly instead of being stamped
70
+ * dollars.
71
+ */
72
+ function convertForeign(body, url) {
73
+ const declared = body.currency;
74
+ if (typeof declared === "string" && declared.toUpperCase() !== "USD") throw new Error(`refreshPricing: dataset at ${url} declares currency '${declared}' - graphorin snapshots are USD-only and no conversion rate is available`);
75
+ const { entries, skipped } = convertGenaiPrices(body);
76
+ if (entries.length === 0) throw new Error(`refreshPricing: the genai-prices dataset at ${url} converted to zero usable entries (${skipped} skipped) - the supported subset may have drifted; see convert-genai-prices.ts`);
77
+ return {
78
+ entries,
79
+ conversion: {
80
+ format: "genai-prices",
81
+ skipped
82
+ }
83
+ };
84
+ }
53
85
  function parseEntries(body) {
54
86
  if (body === null || typeof body !== "object") throw new Error("refreshPricing: response body is not an object");
55
87
  let raw;
@@ -1 +1 @@
1
- {"version":3,"file":"refresh.js","names":["res: Awaited<ReturnType<typeof fetchImpl>>","raw: unknown"],"sources":["../src/refresh.ts"],"sourcesContent":["/**\n * `refreshPricing(...)` - opt-in network call. The framework never\n * invokes this automatically; it exists so the CLI (`graphorin\n * pricing refresh`) and operator scripts can pull a fresh snapshot\n * without re-installing the package.\n *\n * The function fetches the supplied URL as JSON and validates the\n * shape against the {@link PricingSnapshot} contract. The bundled\n * snapshot is **not** mutated; callers persist the refreshed\n * snapshot to disk and reload it explicitly.\n *\n * @packageDocumentation\n */\n\nimport { computeEntriesDigest } from './snapshot/bundled.js';\nimport type { ModelPrice, PricingSnapshot } from './types.js';\n\n/**\n * Configuration shape for {@link refreshPricing}. The `fetchImpl`\n * override exists so tests can exercise the function without making\n * a real network call.\n *\n * @stable\n */\nexport interface RefreshPricingOptions {\n /** Snapshot URL - typically the upstream pricing JSON. */\n readonly url: string;\n /** Optional headers (auth, conditional GET, etc.). */\n readonly headers?: Readonly<Record<string, string>>;\n /** Optional fetch override - useful in tests. */\n readonly fetchImpl?: typeof fetch;\n /** Override the snapshot date stamped on the result. Defaults to today. */\n readonly snapshotDate?: string;\n /** Override the snapshot version string. Defaults to `'graphorin/0.1+refreshed'`. */\n readonly version?: string;\n /**\n * Hard timeout for the network fetch in milliseconds. Default\n * `30000`. Aborts the request (and throws) if the upstream is slow\n * or unreachable so `graphorin pricing refresh` cannot hang. Pass an\n * explicit {@link RefreshPricingOptions.signal} to manage\n * cancellation yourself; the two are combined.\n */\n readonly timeoutMs?: number;\n /** Caller-supplied abort signal, combined with the timeout. */\n readonly signal?: AbortSignal;\n}\n\n/**\n * Pull a fresh snapshot from the supplied URL and return it. Network\n * failures and shape mismatches surface as thrown errors so the CLI\n * can surface them to the operator.\n *\n * @stable\n */\nexport async function refreshPricing(opts: RefreshPricingOptions): Promise<PricingSnapshot> {\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new TypeError(\n 'refreshPricing: no fetch implementation available. Provide `fetchImpl` ' +\n 'explicitly or run on a Node.js version with global fetch (>= 18).',\n );\n }\n const timeoutMs = opts.timeoutMs ?? 30_000;\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n const signal =\n opts.signal === undefined ? timeoutSignal : AbortSignal.any([opts.signal, timeoutSignal]);\n let res: Awaited<ReturnType<typeof fetchImpl>>;\n try {\n res = await fetchImpl(opts.url, { headers: opts.headers ?? {}, signal });\n } catch (err) {\n if (timeoutSignal.aborted) {\n throw new Error(`refreshPricing: timed out after ${timeoutMs} ms fetching ${opts.url}`, {\n cause: err,\n });\n }\n throw err;\n }\n if (!res.ok) {\n throw new Error(`refreshPricing: HTTP ${res.status} ${res.statusText} from ${opts.url}`);\n }\n const body = (await res.json()) as unknown;\n const entries = parseEntries(body);\n const snapshotDate = opts.snapshotDate ?? new Date().toISOString().slice(0, 10);\n const version = opts.version ?? 'graphorin/0.1+refreshed';\n return Object.freeze<PricingSnapshot>({\n version,\n source: opts.url,\n snapshotDate,\n currency: 'USD',\n sha256: computeEntriesDigest(entries),\n entries,\n });\n}\n\nfunction parseEntries(body: unknown): ReadonlyArray<ModelPrice> {\n if (body === null || typeof body !== 'object') {\n throw new Error('refreshPricing: response body is not an object');\n }\n // Two accepted shapes:\n // - `{ entries: ModelPrice[] }`\n // - `ModelPrice[]`\n let raw: unknown;\n if (Array.isArray(body)) {\n raw = body;\n } else {\n raw = (body as { entries?: unknown }).entries;\n if (!Array.isArray(raw)) {\n throw new Error('refreshPricing: response object is missing the `entries` array');\n }\n }\n const entries = (raw as ReadonlyArray<unknown>).map(parseEntry);\n return Object.freeze(entries);\n}\n\nfunction parseEntry(raw: unknown): ModelPrice {\n if (raw === null || typeof raw !== 'object') {\n throw new Error('refreshPricing: pricing entry is not an object');\n }\n const e = raw as Partial<ModelPrice>;\n if (typeof e.provider !== 'string' || typeof e.model !== 'string') {\n throw new Error('refreshPricing: pricing entry is missing provider / model');\n }\n if (typeof e.inputUsdPerToken !== 'number' || typeof e.outputUsdPerToken !== 'number') {\n throw new Error(\n `refreshPricing: pricing entry ${e.provider}/${e.model} is missing input / output prices`,\n );\n }\n return Object.freeze({\n provider: e.provider,\n model: e.model,\n inputUsdPerToken: e.inputUsdPerToken,\n outputUsdPerToken: e.outputUsdPerToken,\n ...(typeof e.cachedReadUsdPerToken === 'number'\n ? { cachedReadUsdPerToken: e.cachedReadUsdPerToken }\n : {}),\n ...(typeof e.cacheWriteUsdPerToken === 'number'\n ? { cacheWriteUsdPerToken: e.cacheWriteUsdPerToken }\n : {}),\n ...(typeof e.reasoningUsdPerToken === 'number'\n ? { reasoningUsdPerToken: e.reasoningUsdPerToken }\n : {}),\n ...(typeof e.region === 'string' ? { region: e.region } : {}),\n ...(typeof e.notes === 'string' ? { notes: e.notes } : {}),\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsDA,eAAsB,eAAe,MAAuD;CAC1F,MAAM,YAAY,KAAK,aAAa,WAAW;AAC/C,KAAI,OAAO,cAAc,WACvB,OAAM,IAAI,UACR,2IAED;CAEH,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,gBAAgB,YAAY,QAAQ,UAAU;CACpD,MAAM,SACJ,KAAK,WAAW,SAAY,gBAAgB,YAAY,IAAI,CAAC,KAAK,QAAQ,cAAc,CAAC;CAC3F,IAAIA;AACJ,KAAI;AACF,QAAM,MAAM,UAAU,KAAK,KAAK;GAAE,SAAS,KAAK,WAAW,EAAE;GAAE;GAAQ,CAAC;UACjE,KAAK;AACZ,MAAI,cAAc,QAChB,OAAM,IAAI,MAAM,mCAAmC,UAAU,eAAe,KAAK,OAAO,EACtF,OAAO,KACR,CAAC;AAEJ,QAAM;;AAER,KAAI,CAAC,IAAI,GACP,OAAM,IAAI,MAAM,wBAAwB,IAAI,OAAO,GAAG,IAAI,WAAW,QAAQ,KAAK,MAAM;CAG1F,MAAM,UAAU,aADF,MAAM,IAAI,MAAM,CACI;CAClC,MAAM,eAAe,KAAK,iCAAgB,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG;CAC/E,MAAM,UAAU,KAAK,WAAW;AAChC,QAAO,OAAO,OAAwB;EACpC;EACA,QAAQ,KAAK;EACb;EACA,UAAU;EACV,QAAQ,qBAAqB,QAAQ;EACrC;EACD,CAAC;;AAGJ,SAAS,aAAa,MAA0C;AAC9D,KAAI,SAAS,QAAQ,OAAO,SAAS,SACnC,OAAM,IAAI,MAAM,iDAAiD;CAKnE,IAAIC;AACJ,KAAI,MAAM,QAAQ,KAAK,CACrB,OAAM;MACD;AACL,QAAO,KAA+B;AACtC,MAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,iEAAiE;;CAGrF,MAAM,UAAW,IAA+B,IAAI,WAAW;AAC/D,QAAO,OAAO,OAAO,QAAQ;;AAG/B,SAAS,WAAW,KAA0B;AAC5C,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SACjC,OAAM,IAAI,MAAM,iDAAiD;CAEnE,MAAM,IAAI;AACV,KAAI,OAAO,EAAE,aAAa,YAAY,OAAO,EAAE,UAAU,SACvD,OAAM,IAAI,MAAM,4DAA4D;AAE9E,KAAI,OAAO,EAAE,qBAAqB,YAAY,OAAO,EAAE,sBAAsB,SAC3E,OAAM,IAAI,MACR,iCAAiC,EAAE,SAAS,GAAG,EAAE,MAAM,mCACxD;AAEH,QAAO,OAAO,OAAO;EACnB,UAAU,EAAE;EACZ,OAAO,EAAE;EACT,kBAAkB,EAAE;EACpB,mBAAmB,EAAE;EACrB,GAAI,OAAO,EAAE,0BAA0B,WACnC,EAAE,uBAAuB,EAAE,uBAAuB,GAClD,EAAE;EACN,GAAI,OAAO,EAAE,0BAA0B,WACnC,EAAE,uBAAuB,EAAE,uBAAuB,GAClD,EAAE;EACN,GAAI,OAAO,EAAE,yBAAyB,WAClC,EAAE,sBAAsB,EAAE,sBAAsB,GAChD,EAAE;EACN,GAAI,OAAO,EAAE,WAAW,WAAW,EAAE,QAAQ,EAAE,QAAQ,GAAG,EAAE;EAC5D,GAAI,OAAO,EAAE,UAAU,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE;EAC1D,CAAC"}
1
+ {"version":3,"file":"refresh.js","names":["res: Awaited<ReturnType<typeof fetchImpl>>","entries: ReadonlyArray<ModelPrice>","conversion: PricingSnapshot['conversion']","raw: unknown"],"sources":["../src/refresh.ts"],"sourcesContent":["/**\n * `refreshPricing(...)` - opt-in network call. The framework never\n * invokes this automatically; it exists so the CLI (`graphorin\n * pricing refresh`) and operator scripts can pull a fresh snapshot\n * without re-installing the package.\n *\n * The function fetches the supplied URL as JSON and validates the\n * shape against the {@link PricingSnapshot} contract. The bundled\n * snapshot is **not** mutated; callers persist the refreshed\n * snapshot to disk and reload it explicitly.\n *\n * @packageDocumentation\n */\n\nimport { convertGenaiPrices, isGenaiPricesShape } from './convert-genai-prices.js';\nimport { computeEntriesDigest } from './snapshot/bundled.js';\nimport type { ModelPrice, PricingSnapshot } from './types.js';\n\n/**\n * Configuration shape for {@link refreshPricing}. The `fetchImpl`\n * override exists so tests can exercise the function without making\n * a real network call.\n *\n * @stable\n */\nexport interface RefreshPricingOptions {\n /** Snapshot URL - typically the upstream pricing JSON. */\n readonly url: string;\n /** Optional headers (auth, conditional GET, etc.). */\n readonly headers?: Readonly<Record<string, string>>;\n /** Optional fetch override - useful in tests. */\n readonly fetchImpl?: typeof fetch;\n /** Override the snapshot date stamped on the result. Defaults to today. */\n readonly snapshotDate?: string;\n /** Override the snapshot version string. Defaults to `'graphorin/0.1+refreshed'`. */\n readonly version?: string;\n /**\n * Hard timeout for the network fetch in milliseconds. Default\n * `30000`. Aborts the request (and throws) if the upstream is slow\n * or unreachable so `graphorin pricing refresh` cannot hang. Pass an\n * explicit {@link RefreshPricingOptions.signal} to manage\n * cancellation yourself; the two are combined.\n */\n readonly timeoutMs?: number;\n /** Caller-supplied abort signal, combined with the timeout. */\n readonly signal?: AbortSignal;\n /**\n * W-097: accepted body format. `'auto'` (default) tries the native\n * graphorin shape, then auto-detects + converts the\n * `@pydantic/genai-prices` dataset; the explicit values pin one\n * format and fail fast on anything else.\n */\n readonly format?: 'auto' | 'graphorin' | 'genai-prices';\n}\n\n/**\n * Pull a fresh snapshot from the supplied URL and return it. Network\n * failures and shape mismatches surface as thrown errors so the CLI\n * can surface them to the operator.\n *\n * @stable\n */\nexport async function refreshPricing(opts: RefreshPricingOptions): Promise<PricingSnapshot> {\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new TypeError(\n 'refreshPricing: no fetch implementation available. Provide `fetchImpl` ' +\n 'explicitly or run on a Node.js version with global fetch (>= 18).',\n );\n }\n const timeoutMs = opts.timeoutMs ?? 30_000;\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n const signal =\n opts.signal === undefined ? timeoutSignal : AbortSignal.any([opts.signal, timeoutSignal]);\n let res: Awaited<ReturnType<typeof fetchImpl>>;\n try {\n res = await fetchImpl(opts.url, { headers: opts.headers ?? {}, signal });\n } catch (err) {\n if (timeoutSignal.aborted) {\n throw new Error(`refreshPricing: timed out after ${timeoutMs} ms fetching ${opts.url}`, {\n cause: err,\n });\n }\n throw err;\n }\n if (!res.ok) {\n throw new Error(`refreshPricing: HTTP ${res.status} ${res.statusText} from ${opts.url}`);\n }\n const body = (await res.json()) as unknown;\n const format = opts.format ?? 'auto';\n let entries: ReadonlyArray<ModelPrice>;\n let conversion: PricingSnapshot['conversion'];\n if (format === 'graphorin') {\n entries = parseEntries(body);\n } else if (format === 'genai-prices') {\n ({ entries, conversion } = convertForeign(body, opts.url));\n } else {\n // 'auto': native first; a shape miss falls through to detection so\n // the operator error names the NATIVE expectation when neither fits.\n try {\n entries = parseEntries(body);\n } catch (nativeError) {\n if (!isGenaiPricesShape(body)) throw nativeError;\n ({ entries, conversion } = convertForeign(body, opts.url));\n }\n }\n const snapshotDate = opts.snapshotDate ?? new Date().toISOString().slice(0, 10);\n const version =\n opts.version ??\n (conversion !== undefined ? 'genai-prices+converted' : 'graphorin/0.1+refreshed');\n return Object.freeze<PricingSnapshot>({\n version,\n source: opts.url,\n snapshotDate,\n currency: 'USD',\n sha256: computeEntriesDigest(entries),\n entries,\n ...(conversion !== undefined ? { conversion } : {}),\n });\n}\n\n/**\n * W-097: convert a genai-prices dataset body, guarding the currency -\n * `PricingSnapshot.currency` is a `'USD'` literal, so a body that\n * DECLARES another currency must fail loudly instead of being stamped\n * dollars.\n */\nfunction convertForeign(\n body: unknown,\n url: string,\n): { entries: ReadonlyArray<ModelPrice>; conversion: NonNullable<PricingSnapshot['conversion']> } {\n const declared = (body as { currency?: unknown }).currency;\n if (typeof declared === 'string' && declared.toUpperCase() !== 'USD') {\n throw new Error(\n `refreshPricing: dataset at ${url} declares currency '${declared}' - graphorin snapshots are USD-only and no conversion rate is available`,\n );\n }\n const { entries, skipped } = convertGenaiPrices(body);\n if (entries.length === 0) {\n throw new Error(\n `refreshPricing: the genai-prices dataset at ${url} converted to zero usable entries (${skipped} skipped) - the supported subset may have drifted; see convert-genai-prices.ts`,\n );\n }\n return { entries, conversion: { format: 'genai-prices', skipped } };\n}\n\nfunction parseEntries(body: unknown): ReadonlyArray<ModelPrice> {\n if (body === null || typeof body !== 'object') {\n throw new Error('refreshPricing: response body is not an object');\n }\n // Two accepted shapes:\n // - `{ entries: ModelPrice[] }`\n // - `ModelPrice[]`\n let raw: unknown;\n if (Array.isArray(body)) {\n raw = body;\n } else {\n raw = (body as { entries?: unknown }).entries;\n if (!Array.isArray(raw)) {\n throw new Error('refreshPricing: response object is missing the `entries` array');\n }\n }\n const entries = (raw as ReadonlyArray<unknown>).map(parseEntry);\n return Object.freeze(entries);\n}\n\nfunction parseEntry(raw: unknown): ModelPrice {\n if (raw === null || typeof raw !== 'object') {\n throw new Error('refreshPricing: pricing entry is not an object');\n }\n const e = raw as Partial<ModelPrice>;\n if (typeof e.provider !== 'string' || typeof e.model !== 'string') {\n throw new Error('refreshPricing: pricing entry is missing provider / model');\n }\n if (typeof e.inputUsdPerToken !== 'number' || typeof e.outputUsdPerToken !== 'number') {\n throw new Error(\n `refreshPricing: pricing entry ${e.provider}/${e.model} is missing input / output prices`,\n );\n }\n return Object.freeze({\n provider: e.provider,\n model: e.model,\n inputUsdPerToken: e.inputUsdPerToken,\n outputUsdPerToken: e.outputUsdPerToken,\n ...(typeof e.cachedReadUsdPerToken === 'number'\n ? { cachedReadUsdPerToken: e.cachedReadUsdPerToken }\n : {}),\n ...(typeof e.cacheWriteUsdPerToken === 'number'\n ? { cacheWriteUsdPerToken: e.cacheWriteUsdPerToken }\n : {}),\n ...(typeof e.reasoningUsdPerToken === 'number'\n ? { reasoningUsdPerToken: e.reasoningUsdPerToken }\n : {}),\n ...(typeof e.region === 'string' ? { region: e.region } : {}),\n ...(typeof e.notes === 'string' ? { notes: e.notes } : {}),\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8DA,eAAsB,eAAe,MAAuD;CAC1F,MAAM,YAAY,KAAK,aAAa,WAAW;AAC/C,KAAI,OAAO,cAAc,WACvB,OAAM,IAAI,UACR,2IAED;CAEH,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,gBAAgB,YAAY,QAAQ,UAAU;CACpD,MAAM,SACJ,KAAK,WAAW,SAAY,gBAAgB,YAAY,IAAI,CAAC,KAAK,QAAQ,cAAc,CAAC;CAC3F,IAAIA;AACJ,KAAI;AACF,QAAM,MAAM,UAAU,KAAK,KAAK;GAAE,SAAS,KAAK,WAAW,EAAE;GAAE;GAAQ,CAAC;UACjE,KAAK;AACZ,MAAI,cAAc,QAChB,OAAM,IAAI,MAAM,mCAAmC,UAAU,eAAe,KAAK,OAAO,EACtF,OAAO,KACR,CAAC;AAEJ,QAAM;;AAER,KAAI,CAAC,IAAI,GACP,OAAM,IAAI,MAAM,wBAAwB,IAAI,OAAO,GAAG,IAAI,WAAW,QAAQ,KAAK,MAAM;CAE1F,MAAM,OAAQ,MAAM,IAAI,MAAM;CAC9B,MAAM,SAAS,KAAK,UAAU;CAC9B,IAAIC;CACJ,IAAIC;AACJ,KAAI,WAAW,YACb,WAAU,aAAa,KAAK;UACnB,WAAW,eACpB,EAAC,CAAE,SAAS,cAAe,eAAe,MAAM,KAAK,IAAI;KAIzD,KAAI;AACF,YAAU,aAAa,KAAK;UACrB,aAAa;AACpB,MAAI,CAAC,mBAAmB,KAAK,CAAE,OAAM;AACrC,GAAC,CAAE,SAAS,cAAe,eAAe,MAAM,KAAK,IAAI;;CAG7D,MAAM,eAAe,KAAK,iCAAgB,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG;CAC/E,MAAM,UACJ,KAAK,YACJ,eAAe,SAAY,2BAA2B;AACzD,QAAO,OAAO,OAAwB;EACpC;EACA,QAAQ,KAAK;EACb;EACA,UAAU;EACV,QAAQ,qBAAqB,QAAQ;EACrC;EACA,GAAI,eAAe,SAAY,EAAE,YAAY,GAAG,EAAE;EACnD,CAAC;;;;;;;;AASJ,SAAS,eACP,MACA,KACgG;CAChG,MAAM,WAAY,KAAgC;AAClD,KAAI,OAAO,aAAa,YAAY,SAAS,aAAa,KAAK,MAC7D,OAAM,IAAI,MACR,8BAA8B,IAAI,sBAAsB,SAAS,0EAClE;CAEH,MAAM,EAAE,SAAS,YAAY,mBAAmB,KAAK;AACrD,KAAI,QAAQ,WAAW,EACrB,OAAM,IAAI,MACR,+CAA+C,IAAI,qCAAqC,QAAQ,gFACjG;AAEH,QAAO;EAAE;EAAS,YAAY;GAAE,QAAQ;GAAgB;GAAS;EAAE;;AAGrE,SAAS,aAAa,MAA0C;AAC9D,KAAI,SAAS,QAAQ,OAAO,SAAS,SACnC,OAAM,IAAI,MAAM,iDAAiD;CAKnE,IAAIC;AACJ,KAAI,MAAM,QAAQ,KAAK,CACrB,OAAM;MACD;AACL,QAAO,KAA+B;AACtC,MAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,iEAAiE;;CAGrF,MAAM,UAAW,IAA+B,IAAI,WAAW;AAC/D,QAAO,OAAO,OAAO,QAAQ;;AAG/B,SAAS,WAAW,KAA0B;AAC5C,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SACjC,OAAM,IAAI,MAAM,iDAAiD;CAEnE,MAAM,IAAI;AACV,KAAI,OAAO,EAAE,aAAa,YAAY,OAAO,EAAE,UAAU,SACvD,OAAM,IAAI,MAAM,4DAA4D;AAE9E,KAAI,OAAO,EAAE,qBAAqB,YAAY,OAAO,EAAE,sBAAsB,SAC3E,OAAM,IAAI,MACR,iCAAiC,EAAE,SAAS,GAAG,EAAE,MAAM,mCACxD;AAEH,QAAO,OAAO,OAAO;EACnB,UAAU,EAAE;EACZ,OAAO,EAAE;EACT,kBAAkB,EAAE;EACpB,mBAAmB,EAAE;EACrB,GAAI,OAAO,EAAE,0BAA0B,WACnC,EAAE,uBAAuB,EAAE,uBAAuB,GAClD,EAAE;EACN,GAAI,OAAO,EAAE,0BAA0B,WACnC,EAAE,uBAAuB,EAAE,uBAAuB,GAClD,EAAE;EACN,GAAI,OAAO,EAAE,yBAAyB,WAClC,EAAE,sBAAsB,EAAE,sBAAsB,GAChD,EAAE;EACN,GAAI,OAAO,EAAE,WAAW,WAAW,EAAE,QAAQ,EAAE,QAAQ,GAAG,EAAE;EAC5D,GAAI,OAAO,EAAE,UAAU,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE;EAC1D,CAAC"}
package/dist/types.d.ts CHANGED
@@ -46,6 +46,16 @@ interface PricingSnapshot {
46
46
  readonly currency: 'USD';
47
47
  readonly sha256: string;
48
48
  readonly entries: ReadonlyArray<ModelPrice>;
49
+ /**
50
+ * W-097: present when `refreshPricing` converted a foreign dataset
51
+ * (today: `@pydantic/genai-prices`) instead of consuming the native
52
+ * shape. `skipped` counts model entries the supported subset could
53
+ * not represent (tiered / conditional pricing).
54
+ */
55
+ readonly conversion?: {
56
+ readonly format: 'genai-prices';
57
+ readonly skipped: number;
58
+ };
49
59
  }
50
60
  /**
51
61
  * Lookup criteria.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;AAuEA;AAeA;;;;;;AAgBA;UAtFiB,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8BA,eAAA;;;;;;oBAMG,cAAc;;;;;;;UAQjB,eAAA;;;;;;;;;;UAWA,iBAAA;;;;;;;;;;;;;;UAeA,gBAAA;;;;oBAIG;mBACD;2BACQ,oBAAoB;;;;;;;;;UAU9B,oBAAA;uBACM,SAAS"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;AAiFA;AAeA;;;;;;AAgBA;UAhGiB,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8BA,eAAA;;;;;;oBAMG,cAAc;;;;;;;;;;;;;;;;;UAkBjB,eAAA;;;;;;;;;;UAWA,iBAAA;;;;;;;;;;;;;;UAeA,gBAAA;;;;oBAIG;mBACD;2BACQ,oBAAoB;;;;;;;;;UAU9B,oBAAA;uBACM,SAAS"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphorin/pricing",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Bundled LLM pricing snapshot for the Graphorin framework. Ships per-provider, per-model input / output / cached-read prices alongside provenance metadata, a `lookupPrice(...)` resolver, snapshot diffing utilities, an opt-in `refreshPricing(...)` hook (network gated; never automatic), and `listMissingModels(...)` for trace audits.",
5
5
  "license": "MIT",
6
6
  "author": "Oleksiy Stepurenko",
@@ -24,8 +24,9 @@
24
24
  "llm"
25
25
  ],
26
26
  "type": "module",
27
+ "sideEffects": false,
27
28
  "engines": {
28
- "node": ">=22.0.0"
29
+ "node": ">=22.12.0"
29
30
  },
30
31
  "main": "./dist/index.js",
31
32
  "module": "./dist/index.js",
@@ -33,22 +34,23 @@
33
34
  "exports": {
34
35
  ".": {
35
36
  "types": "./dist/index.d.ts",
36
- "import": "./dist/index.js"
37
+ "default": "./dist/index.js"
37
38
  },
38
39
  "./snapshot": {
39
40
  "types": "./dist/snapshot/index.d.ts",
40
- "import": "./dist/snapshot/index.js"
41
+ "default": "./dist/snapshot/index.js"
41
42
  },
42
43
  "./package.json": "./package.json"
43
44
  },
44
45
  "files": [
45
46
  "dist",
47
+ "src",
46
48
  "README.md",
47
49
  "CHANGELOG.md",
48
50
  "LICENSE"
49
51
  ],
50
52
  "dependencies": {
51
- "@graphorin/core": "0.6.0"
53
+ "@graphorin/core": "0.7.0"
52
54
  },
53
55
  "publishConfig": {
54
56
  "access": "public",
package/src/config.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Configuration shapes consumed by `pricing.*`.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ /**
8
+ * Auto-refresh configuration. The `enabled` flag exists in the type
9
+ * for forward compatibility but is **enforced false** in v0.1 per the
10
+ * zero-default-telemetry policy: refreshing the snapshot makes an
11
+ * outbound HTTP call, so it must remain an explicit user action.
12
+ *
13
+ * @stable
14
+ */
15
+ export interface PricingAutoRefreshConfig {
16
+ /** Always `false` in v0.1. Reserved for v0.2+. */
17
+ readonly enabled: false;
18
+ /** Suggested cadence for v0.2+. Ignored at runtime in v0.1. */
19
+ readonly intervalHours?: number;
20
+ }
21
+
22
+ /**
23
+ * Default auto-refresh configuration.
24
+ *
25
+ * @stable
26
+ */
27
+ export const DEFAULT_PRICING_AUTO_REFRESH: PricingAutoRefreshConfig = Object.freeze({
28
+ enabled: false as const,
29
+ intervalHours: 24,
30
+ });
31
+
32
+ /**
33
+ * Container shape for the broader `pricing.*` configuration.
34
+ *
35
+ * @stable
36
+ */
37
+ export interface PricingConfig {
38
+ readonly autoRefresh?: PricingAutoRefreshConfig;
39
+ }