@graphorin/pricing 0.5.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 ADDED
@@ -0,0 +1,7 @@
1
+ # @graphorin/pricing
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release. See the workspace root `CHANGELOG.md` for the full
6
+ release notes; the per-package changelog is generated by Changesets
7
+ and tracks subsequent updates.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksiy Stepurenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @graphorin/pricing
2
+
3
+ > Bundled LLM pricing snapshot for the [Graphorin](https://github.com/o-stepper/graphorin) framework.
4
+
5
+ `@graphorin/pricing` is a tiny dependency-free package that ships a
6
+ representative LLM pricing catalogue, a `lookupPrice(...)` resolver, a
7
+ snapshot-diff utility, an opt-in `refreshPricing(...)` hook, and the
8
+ `listMissingModels(...)` utility used by trace audits.
9
+
10
+ The package is intentionally separate from `@graphorin/observability`
11
+ so consumers can:
12
+
13
+ - pin the pricing snapshot independently of the framework version,
14
+ - ship a custom snapshot without forking the observability package,
15
+ - decline the bundled snapshot entirely (just don't import it).
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pnpm add @graphorin/pricing
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```ts
26
+ import {
27
+ BUNDLED_SNAPSHOT,
28
+ calculateCost,
29
+ lookupPrice,
30
+ } from '@graphorin/pricing';
31
+
32
+ const price = lookupPrice({ provider: 'anthropic', model: 'claude-3-5-sonnet-20241022' });
33
+ if (price !== null) {
34
+ console.log(`Input: $${price.inputUsdPerToken} / token`);
35
+ }
36
+
37
+ const cost = calculateCost(
38
+ {
39
+ provider: 'openai',
40
+ model: 'gpt-4o-2024-11-20',
41
+ inputTokens: 1_000,
42
+ outputTokens: 500,
43
+ },
44
+ BUNDLED_SNAPSHOT,
45
+ );
46
+ ```
47
+
48
+ ## Refreshing
49
+
50
+ ```ts
51
+ import { refreshPricing } from '@graphorin/pricing';
52
+ import { writeFile } from 'node:fs/promises';
53
+
54
+ const snapshot = await refreshPricing({
55
+ url: 'https://example.com/pricing.json',
56
+ });
57
+ await writeFile('./pricing.json', JSON.stringify(snapshot, null, 2));
58
+ ```
59
+
60
+ The framework never invokes `refreshPricing(...)` automatically per
61
+ the zero-default-telemetry policy; the operator runs it on demand.
62
+
63
+ ## License
64
+
65
+ MIT © 2026 [Oleksiy Stepurenko](https://github.com/o-stepper).
66
+
67
+ ---
68
+
69
+ **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
@@ -0,0 +1,37 @@
1
+ //#region src/config.d.ts
2
+ /**
3
+ * Configuration shapes consumed by `pricing.*`.
4
+ *
5
+ * @packageDocumentation
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
+ 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
+ * Default auto-refresh configuration.
23
+ *
24
+ * @stable
25
+ */
26
+ declare const DEFAULT_PRICING_AUTO_REFRESH: PricingAutoRefreshConfig;
27
+ /**
28
+ * Container shape for the broader `pricing.*` configuration.
29
+ *
30
+ * @stable
31
+ */
32
+ interface PricingConfig {
33
+ readonly autoRefresh?: PricingAutoRefreshConfig;
34
+ }
35
+ //#endregion
36
+ export { DEFAULT_PRICING_AUTO_REFRESH, PricingAutoRefreshConfig, PricingConfig };
37
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","names":[],"sources":["../src/config.ts"],"sourcesContent":[],"mappings":";;AAcA;AAYA;AAUA;;;;;;;;;;UAtBiB,wBAAA;;;;;;;;;;;cAYJ,8BAA8B;;;;;;UAU1B,aAAA;yBACQ"}
package/dist/config.js ADDED
@@ -0,0 +1,14 @@
1
+ //#region src/config.ts
2
+ /**
3
+ * Default auto-refresh configuration.
4
+ *
5
+ * @stable
6
+ */
7
+ const DEFAULT_PRICING_AUTO_REFRESH = Object.freeze({
8
+ enabled: false,
9
+ intervalHours: 24
10
+ });
11
+
12
+ //#endregion
13
+ export { DEFAULT_PRICING_AUTO_REFRESH };
14
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":["DEFAULT_PRICING_AUTO_REFRESH: PricingAutoRefreshConfig"],"sources":["../src/config.ts"],"sourcesContent":["/**\n * Configuration shapes consumed by `pricing.*`.\n *\n * @packageDocumentation\n */\n\n/**\n * Auto-refresh configuration. The `enabled` flag exists in the type\n * for forward compatibility but is **enforced false** in v0.1 per the\n * zero-default-telemetry policy: refreshing the snapshot makes an\n * outbound HTTP call, so it must remain an explicit user action.\n *\n * @stable\n */\nexport interface PricingAutoRefreshConfig {\n /** Always `false` in v0.1. Reserved for v0.2+. */\n readonly enabled: false;\n /** Suggested cadence for v0.2+. Ignored at runtime in v0.1. */\n readonly intervalHours?: number;\n}\n\n/**\n * Default auto-refresh configuration.\n *\n * @stable\n */\nexport const DEFAULT_PRICING_AUTO_REFRESH: PricingAutoRefreshConfig = Object.freeze({\n enabled: false as const,\n intervalHours: 24,\n});\n\n/**\n * Container shape for the broader `pricing.*` configuration.\n *\n * @stable\n */\nexport interface PricingConfig {\n readonly autoRefresh?: PricingAutoRefreshConfig;\n}\n"],"mappings":";;;;;;AA0BA,MAAaA,+BAAyD,OAAO,OAAO;CAClF,SAAS;CACT,eAAe;CAChB,CAAC"}
package/dist/diff.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { PricingDiffEntry, PricingSnapshot } from "./types.js";
2
+
3
+ //#region src/diff.d.ts
4
+
5
+ /**
6
+ * Compare two snapshots and return one entry per (provider, model)
7
+ * pair that has been added, removed, or changed. The result is sorted
8
+ * by `(provider, model, kind)` for deterministic output.
9
+ *
10
+ * @stable
11
+ */
12
+ declare function diffPricing(before: PricingSnapshot, after: PricingSnapshot): ReadonlyArray<PricingDiffEntry>;
13
+ //#endregion
14
+ export { diffPricing };
15
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.d.ts","names":[],"sources":["../src/diff.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;iBA0BgB,WAAA,SACN,wBACD,kBACN,cAAc"}
package/dist/diff.js ADDED
@@ -0,0 +1,65 @@
1
+ //#region src/diff.ts
2
+ const COMPARED_FIELDS = [
3
+ "inputUsdPerToken",
4
+ "outputUsdPerToken",
5
+ "cachedReadUsdPerToken",
6
+ "reasoningUsdPerToken",
7
+ "region",
8
+ "notes"
9
+ ];
10
+ /**
11
+ * Compare two snapshots and return one entry per (provider, model)
12
+ * pair that has been added, removed, or changed. The result is sorted
13
+ * by `(provider, model, kind)` for deterministic output.
14
+ *
15
+ * @stable
16
+ */
17
+ function diffPricing(before, after) {
18
+ const beforeMap = indexBy(before.entries);
19
+ const afterMap = indexBy(after.entries);
20
+ const out = [];
21
+ for (const [key, entry] of beforeMap) {
22
+ const next = afterMap.get(key);
23
+ if (next === void 0) {
24
+ out.push({
25
+ provider: entry.provider,
26
+ model: entry.model,
27
+ kind: "removed",
28
+ before: entry
29
+ });
30
+ continue;
31
+ }
32
+ const changedFields = COMPARED_FIELDS.filter((field) => entry[field] !== next[field]);
33
+ if (changedFields.length > 0) out.push({
34
+ provider: entry.provider,
35
+ model: entry.model,
36
+ kind: "changed",
37
+ before: entry,
38
+ after: next,
39
+ changedFields
40
+ });
41
+ }
42
+ for (const [key, entry] of afterMap) {
43
+ if (beforeMap.has(key)) continue;
44
+ out.push({
45
+ provider: entry.provider,
46
+ model: entry.model,
47
+ kind: "added",
48
+ after: entry
49
+ });
50
+ }
51
+ return out.sort((a, b) => {
52
+ if (a.provider !== b.provider) return a.provider < b.provider ? -1 : 1;
53
+ if (a.model !== b.model) return a.model < b.model ? -1 : 1;
54
+ return a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0;
55
+ });
56
+ }
57
+ function indexBy(entries) {
58
+ const out = /* @__PURE__ */ new Map();
59
+ for (const entry of entries) out.set(`${entry.provider}/${entry.model}/${entry.region ?? ""}`, entry);
60
+ return out;
61
+ }
62
+
63
+ //#endregion
64
+ export { diffPricing };
65
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","names":["COMPARED_FIELDS: ReadonlyArray<keyof ModelPrice>","out: PricingDiffEntry[]"],"sources":["../src/diff.ts"],"sourcesContent":["/**\n * `diffPricing(...)` — produce a row-by-row delta between two pricing\n * snapshots. Used by the upcoming `graphorin pricing diff` CLI to\n * surface upstream changes after a `refresh`.\n *\n * @packageDocumentation\n */\n\nimport type { ModelPrice, PricingDiffEntry, PricingSnapshot } from './types.js';\n\nconst COMPARED_FIELDS: ReadonlyArray<keyof ModelPrice> = [\n 'inputUsdPerToken',\n 'outputUsdPerToken',\n 'cachedReadUsdPerToken',\n 'reasoningUsdPerToken',\n 'region',\n 'notes',\n];\n\n/**\n * Compare two snapshots and return one entry per (provider, model)\n * pair that has been added, removed, or changed. The result is sorted\n * by `(provider, model, kind)` for deterministic output.\n *\n * @stable\n */\nexport function diffPricing(\n before: PricingSnapshot,\n after: PricingSnapshot,\n): ReadonlyArray<PricingDiffEntry> {\n const beforeMap = indexBy(before.entries);\n const afterMap = indexBy(after.entries);\n const out: PricingDiffEntry[] = [];\n\n for (const [key, entry] of beforeMap) {\n const next = afterMap.get(key);\n if (next === undefined) {\n out.push({ provider: entry.provider, model: entry.model, kind: 'removed', before: entry });\n continue;\n }\n const changedFields = COMPARED_FIELDS.filter((field) => entry[field] !== next[field]);\n if (changedFields.length > 0) {\n out.push({\n provider: entry.provider,\n model: entry.model,\n kind: 'changed',\n before: entry,\n after: next,\n changedFields,\n });\n }\n }\n\n for (const [key, entry] of afterMap) {\n if (beforeMap.has(key)) continue;\n out.push({ provider: entry.provider, model: entry.model, kind: 'added', after: entry });\n }\n\n return out.sort((a, b) => {\n if (a.provider !== b.provider) return a.provider < b.provider ? -1 : 1;\n if (a.model !== b.model) return a.model < b.model ? -1 : 1;\n return a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0;\n });\n}\n\nfunction indexBy(entries: ReadonlyArray<ModelPrice>): Map<string, ModelPrice> {\n const out = new Map<string, ModelPrice>();\n for (const entry of entries) {\n out.set(`${entry.provider}/${entry.model}/${entry.region ?? ''}`, entry);\n }\n return out;\n}\n"],"mappings":";AAUA,MAAMA,kBAAmD;CACvD;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;AASD,SAAgB,YACd,QACA,OACiC;CACjC,MAAM,YAAY,QAAQ,OAAO,QAAQ;CACzC,MAAM,WAAW,QAAQ,MAAM,QAAQ;CACvC,MAAMC,MAA0B,EAAE;AAElC,MAAK,MAAM,CAAC,KAAK,UAAU,WAAW;EACpC,MAAM,OAAO,SAAS,IAAI,IAAI;AAC9B,MAAI,SAAS,QAAW;AACtB,OAAI,KAAK;IAAE,UAAU,MAAM;IAAU,OAAO,MAAM;IAAO,MAAM;IAAW,QAAQ;IAAO,CAAC;AAC1F;;EAEF,MAAM,gBAAgB,gBAAgB,QAAQ,UAAU,MAAM,WAAW,KAAK,OAAO;AACrF,MAAI,cAAc,SAAS,EACzB,KAAI,KAAK;GACP,UAAU,MAAM;GAChB,OAAO,MAAM;GACb,MAAM;GACN,QAAQ;GACR,OAAO;GACP;GACD,CAAC;;AAIN,MAAK,MAAM,CAAC,KAAK,UAAU,UAAU;AACnC,MAAI,UAAU,IAAI,IAAI,CAAE;AACxB,MAAI,KAAK;GAAE,UAAU,MAAM;GAAU,OAAO,MAAM;GAAO,MAAM;GAAS,OAAO;GAAO,CAAC;;AAGzF,QAAO,IAAI,MAAM,GAAG,MAAM;AACxB,MAAI,EAAE,aAAa,EAAE,SAAU,QAAO,EAAE,WAAW,EAAE,WAAW,KAAK;AACrE,MAAI,EAAE,UAAU,EAAE,MAAO,QAAO,EAAE,QAAQ,EAAE,QAAQ,KAAK;AACzD,SAAO,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI;GACpD;;AAGJ,SAAS,QAAQ,SAA6D;CAC5E,MAAM,sBAAM,IAAI,KAAyB;AACzC,MAAK,MAAM,SAAS,QAClB,KAAI,IAAI,GAAG,MAAM,SAAS,GAAG,MAAM,MAAM,GAAG,MAAM,UAAU,MAAM,MAAM;AAE1E,QAAO"}
@@ -0,0 +1,41 @@
1
+ import { DEFAULT_PRICING_AUTO_REFRESH, PricingAutoRefreshConfig, PricingConfig } from "./config.js";
2
+ import { Cost, LookupPriceArgs, LookupPriceResult, ModelPrice, PricingDiffEntry, PricingSnapshot, PricingTraceSpanLike } from "./types.js";
3
+ import { diffPricing } from "./diff.js";
4
+ import { _resetLookupWarningsForTesting, calculateCost, lookupPrice, setLookupWarnSink } from "./lookup.js";
5
+ import { MissingModelEntry, listMissingModels } from "./missing.js";
6
+ import { RefreshPricingOptions, refreshPricing } from "./refresh.js";
7
+ import { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest } from "./snapshot/bundled.js";
8
+
9
+ //#region src/index.d.ts
10
+
11
+ /**
12
+ * @graphorin/pricing — bundled LLM pricing snapshot for the Graphorin
13
+ * framework.
14
+ *
15
+ * Highlights:
16
+ *
17
+ * - {@link BUNDLED_SNAPSHOT} — bundled snapshot covering Anthropic,
18
+ * OpenAI, Google, Mistral, Cohere + the local providers
19
+ * (Ollama / llama.cpp). Each entry carries a per-token price and
20
+ * the canonical SHA-256 digest of the `entries` array.
21
+ * - {@link lookupPrice} — resolve a `(provider, model)` pair against a
22
+ * snapshot. Returns `null` for unknown entries plus emits one WARN
23
+ * per process-lifetime per unknown pair.
24
+ * - {@link calculateCost} — multiply a price by token counts for a
25
+ * single LLM call.
26
+ * - {@link diffPricing} — row-by-row delta between two snapshots.
27
+ * - {@link refreshPricing} — opt-in network call. Never automatic.
28
+ * - {@link listMissingModels} — scan trace spans for unknown models;
29
+ * used by `graphorin pricing missing` (Phase 15).
30
+ *
31
+ * The bundled snapshot is intentionally small — operators wanting the
32
+ * full upstream catalogue should run `refreshPricing(...)` and
33
+ * persist the result to disk.
34
+ *
35
+ * @packageDocumentation
36
+ */
37
+ /** Canonical version constant. Mirrors the `package.json` version. */
38
+ declare const VERSION = "0.5.0";
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 };
41
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;;AA4BA;;;;;;;;;;;;;;;;;;cAAa,OAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ import { DEFAULT_PRICING_AUTO_REFRESH } from "./config.js";
2
+ import { diffPricing } from "./diff.js";
3
+ import { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest } from "./snapshot/bundled.js";
4
+ import { _resetLookupWarningsForTesting, calculateCost, lookupPrice, setLookupWarnSink } from "./lookup.js";
5
+ import { listMissingModels } from "./missing.js";
6
+ import { refreshPricing } from "./refresh.js";
7
+ import "./snapshot/index.js";
8
+
9
+ //#region src/index.ts
10
+ /**
11
+ * @graphorin/pricing — bundled LLM pricing snapshot for the Graphorin
12
+ * framework.
13
+ *
14
+ * Highlights:
15
+ *
16
+ * - {@link BUNDLED_SNAPSHOT} — bundled snapshot covering Anthropic,
17
+ * OpenAI, Google, Mistral, Cohere + the local providers
18
+ * (Ollama / llama.cpp). Each entry carries a per-token price and
19
+ * the canonical SHA-256 digest of the `entries` array.
20
+ * - {@link lookupPrice} — resolve a `(provider, model)` pair against a
21
+ * snapshot. Returns `null` for unknown entries plus emits one WARN
22
+ * per process-lifetime per unknown pair.
23
+ * - {@link calculateCost} — multiply a price by token counts for a
24
+ * single LLM call.
25
+ * - {@link diffPricing} — row-by-row delta between two snapshots.
26
+ * - {@link refreshPricing} — opt-in network call. Never automatic.
27
+ * - {@link listMissingModels} — scan trace spans for unknown models;
28
+ * used by `graphorin pricing missing` (Phase 15).
29
+ *
30
+ * The bundled snapshot is intentionally small — operators wanting the
31
+ * full upstream catalogue should run `refreshPricing(...)` and
32
+ * persist the result to disk.
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+ /** Canonical version constant. Mirrors the `package.json` version. */
37
+ const VERSION = "0.5.0";
38
+
39
+ //#endregion
40
+ export { BUNDLED_SNAPSHOT, DEFAULT_PRICING_AUTO_REFRESH, SNAPSHOT_DATE, VERSION, _resetLookupWarningsForTesting, calculateCost, computeEntriesDigest, diffPricing, listMissingModels, lookupPrice, refreshPricing, setLookupWarnSink };
41
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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.5.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"}
@@ -0,0 +1,52 @@
1
+ import { LookupPriceArgs, LookupPriceResult, PricingSnapshot } from "./types.js";
2
+
3
+ //#region src/lookup.d.ts
4
+
5
+ /**
6
+ * Optional sink for the deduplicated WARN emitted on unknown models.
7
+ * Defaults to `console.warn`. Override in tests.
8
+ *
9
+ * @internal
10
+ */
11
+ declare function setLookupWarnSink(sink: (line: string) => void): void;
12
+ /**
13
+ * @internal — exposed for tests so the WARN-once cache can be reset.
14
+ */
15
+ declare function _resetLookupWarningsForTesting(): void;
16
+ /**
17
+ * Resolve a per-token price for the (provider, model) pair. Returns
18
+ * `null` when the snapshot does not contain an entry for the model.
19
+ *
20
+ * The function emits one WARN per process per unknown (provider, model)
21
+ * pair so cost dashboards surface drift without spamming the log.
22
+ *
23
+ * @stable
24
+ */
25
+ declare function lookupPrice(args: LookupPriceArgs, snapshot?: PricingSnapshot): LookupPriceResult | null;
26
+ /**
27
+ * Multiply a per-token price by an integer token count. Returns `null`
28
+ * when the price is unknown. Useful when caller wants to compute cost
29
+ * for a single LLM call without instantiating the cost tracker.
30
+ *
31
+ * Token-count contract (PS-19):
32
+ * - `inputTokens` **excludes** `cachedReadTokens` — cached reads are billed
33
+ * separately at the cheaper cached rate, so pass the non-cached prompt count
34
+ * to avoid double-billing.
35
+ * - `reasoningTokens` are billed at `outputUsdPerToken` unless the model entry
36
+ * declares an explicit `reasoningUsdPerToken`.
37
+ *
38
+ * @stable
39
+ */
40
+ declare function calculateCost(args: LookupPriceArgs & {
41
+ /** Non-cached prompt tokens (excludes `cachedReadTokens`). */
42
+ readonly inputTokens: number;
43
+ readonly outputTokens: number;
44
+ readonly cachedReadTokens?: number;
45
+ readonly reasoningTokens?: number;
46
+ }, snapshot?: PricingSnapshot): {
47
+ readonly amount: number;
48
+ readonly currency: 'USD';
49
+ } | null;
50
+ //#endregion
51
+ export { _resetLookupWarningsForTesting, calculateCost, lookupPrice, setLookupWarnSink };
52
+ //# sourceMappingURL=lookup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lookup.d.ts","names":[],"sources":["../src/lookup.ts"],"sourcesContent":[],"mappings":";;;;;AAyGA;;;;;iBArFgB,iBAAA;;;;iBASA,8BAAA,CAAA;;;;;;;;;;iBAaA,WAAA,OACR,4BACI,kBACT;;;;;;;;;;;;;;;iBA4Da,aAAA,OACR;;;;;;cAOI"}
package/dist/lookup.js ADDED
@@ -0,0 +1,92 @@
1
+ import { BUNDLED_SNAPSHOT } from "./snapshot/bundled.js";
2
+
3
+ //#region src/lookup.ts
4
+ /**
5
+ * `lookupPrice(...)` — resolve a per-token price for a given
6
+ * (provider, model) pair against a snapshot. Returns `null` when the
7
+ * model is unknown and emits one WARN per process-lifetime per
8
+ * unknown (provider, model) pair.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ const WARNED = /* @__PURE__ */ new Set();
13
+ /**
14
+ * Optional sink for the deduplicated WARN emitted on unknown models.
15
+ * Defaults to `console.warn`. Override in tests.
16
+ *
17
+ * @internal
18
+ */
19
+ function setLookupWarnSink(sink) {
20
+ WARN_SINK = sink;
21
+ }
22
+ let WARN_SINK = (line) => console.warn(line);
23
+ /**
24
+ * @internal — exposed for tests so the WARN-once cache can be reset.
25
+ */
26
+ function _resetLookupWarningsForTesting() {
27
+ WARNED.clear();
28
+ }
29
+ /**
30
+ * Resolve a per-token price for the (provider, model) pair. Returns
31
+ * `null` when the snapshot does not contain an entry for the model.
32
+ *
33
+ * The function emits one WARN per process per unknown (provider, model)
34
+ * pair so cost dashboards surface drift without spamming the log.
35
+ *
36
+ * @stable
37
+ */
38
+ function lookupPrice(args, snapshot = BUNDLED_SNAPSHOT) {
39
+ const exact = snapshot.entries.find((entry) => entry.provider === args.provider && entry.model === args.model && (args.region === void 0 || entry.region === void 0 || entry.region === args.region));
40
+ if (exact !== void 0) return entryToResult(exact, snapshot);
41
+ const wildcard = snapshot.entries.find((entry) => entry.provider === args.provider && entry.model === "*");
42
+ if (wildcard !== void 0) return entryToResult(wildcard, snapshot);
43
+ warnOnce(args);
44
+ return null;
45
+ }
46
+ function entryToResult(entry, snapshot) {
47
+ return {
48
+ inputUsdPerToken: entry.inputUsdPerToken,
49
+ outputUsdPerToken: entry.outputUsdPerToken,
50
+ ...entry.cachedReadUsdPerToken === void 0 ? {} : { cachedReadUsdPerToken: entry.cachedReadUsdPerToken },
51
+ ...entry.reasoningUsdPerToken === void 0 ? {} : { reasoningUsdPerToken: entry.reasoningUsdPerToken },
52
+ source: snapshot.source,
53
+ snapshotDate: snapshot.snapshotDate
54
+ };
55
+ }
56
+ function warnOnce(args) {
57
+ const key = `${args.provider}/${args.model}`;
58
+ if (WARNED.has(key)) return;
59
+ WARNED.add(key);
60
+ WARN_SINK(`[graphorin/pricing] WARN: no pricing entry for ${args.provider}/${args.model}; cost is null. Consider running \`graphorin pricing refresh\` or contributing the entry upstream.`);
61
+ }
62
+ /**
63
+ * Multiply a per-token price by an integer token count. Returns `null`
64
+ * when the price is unknown. Useful when caller wants to compute cost
65
+ * for a single LLM call without instantiating the cost tracker.
66
+ *
67
+ * Token-count contract (PS-19):
68
+ * - `inputTokens` **excludes** `cachedReadTokens` — cached reads are billed
69
+ * separately at the cheaper cached rate, so pass the non-cached prompt count
70
+ * to avoid double-billing.
71
+ * - `reasoningTokens` are billed at `outputUsdPerToken` unless the model entry
72
+ * declares an explicit `reasoningUsdPerToken`.
73
+ *
74
+ * @stable
75
+ */
76
+ function calculateCost(args, snapshot = BUNDLED_SNAPSHOT) {
77
+ const price = lookupPrice(args, snapshot);
78
+ if (price === null) return null;
79
+ let amount = 0;
80
+ amount += price.inputUsdPerToken * args.inputTokens;
81
+ amount += price.outputUsdPerToken * args.outputTokens;
82
+ if (args.cachedReadTokens !== void 0 && price.cachedReadUsdPerToken !== void 0) amount += price.cachedReadUsdPerToken * args.cachedReadTokens;
83
+ if (args.reasoningTokens !== void 0) amount += (price.reasoningUsdPerToken ?? price.outputUsdPerToken) * args.reasoningTokens;
84
+ return {
85
+ amount,
86
+ currency: "USD"
87
+ };
88
+ }
89
+
90
+ //#endregion
91
+ export { _resetLookupWarningsForTesting, calculateCost, lookupPrice, setLookupWarnSink };
92
+ //# sourceMappingURL=lookup.js.map
@@ -0,0 +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 // 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.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` — cached reads are billed\n * separately at the cheaper cached rate, so pass the non-cached prompt count\n * to avoid double-billing.\n * - `reasoningTokens` are billed at `outputUsdPerToken` unless the model entry\n * declares an explicit `reasoningUsdPerToken`.\n *\n * @stable\n */\nexport function calculateCost(\n args: LookupPriceArgs & {\n /** Non-cached prompt tokens (excludes `cachedReadTokens`). */\n readonly inputTokens: number;\n readonly outputTokens: number;\n readonly cachedReadTokens?: 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.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;CAG9D,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,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;;;;;;;;;;;;;;;;AAiBH,SAAgB,cACd,MAOA,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,oBAAoB,OAI3B,YAAW,MAAM,wBAAwB,MAAM,qBAAqB,KAAK;AAE3E,QAAO;EAAE;EAAQ,UAAU;EAAO"}
@@ -0,0 +1,24 @@
1
+ import { PricingSnapshot, PricingTraceSpanLike } from "./types.js";
2
+
3
+ //#region src/missing.d.ts
4
+
5
+ /**
6
+ * Single missing-model row.
7
+ *
8
+ * @stable
9
+ */
10
+ interface MissingModelEntry {
11
+ readonly provider: string;
12
+ readonly model: string;
13
+ readonly count: number;
14
+ }
15
+ /**
16
+ * Return one entry per (provider, model) pair that the snapshot does
17
+ * not recognise, sorted by descending occurrence count.
18
+ *
19
+ * @stable
20
+ */
21
+ declare function listMissingModels(spans: Iterable<PricingTraceSpanLike>, snapshot?: PricingSnapshot): ReadonlyArray<MissingModelEntry>;
22
+ //#endregion
23
+ export { MissingModelEntry, listMissingModels };
24
+ //# sourceMappingURL=missing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing.d.ts","names":[],"sources":["../src/missing.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAsBiB,iBAAA;;;;;;;;;;;iBAYD,iBAAA,QACP,SAAS,kCACN,kBACT,cAAc"}
@@ -0,0 +1,62 @@
1
+ import { BUNDLED_SNAPSHOT } from "./snapshot/bundled.js";
2
+ import { lookupPrice } from "./lookup.js";
3
+
4
+ //#region src/missing.ts
5
+ /**
6
+ * `listMissingModels(...)` — scan a sequence of trace spans and return
7
+ * the unique (provider, model) pairs that the snapshot does not
8
+ * recognise.
9
+ *
10
+ * The function reads `gen_ai.system` + `gen_ai.request.model` (the
11
+ * canonical OpenTelemetry GenAI attributes) and falls back to the
12
+ * Graphorin-prefixed `graphorin.provider.id` /
13
+ * `graphorin.provider.model` pair when `gen_ai.*` is absent.
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+ /**
18
+ * Return one entry per (provider, model) pair that the snapshot does
19
+ * not recognise, sorted by descending occurrence count.
20
+ *
21
+ * @stable
22
+ */
23
+ function listMissingModels(spans, snapshot = BUNDLED_SNAPSHOT) {
24
+ const seen = /* @__PURE__ */ new Map();
25
+ for (const span of spans) {
26
+ const provider = stringAttr(span, ["gen_ai.system", "graphorin.provider.id"]);
27
+ const model = stringAttr(span, [
28
+ "gen_ai.request.model",
29
+ "gen_ai.response.model",
30
+ "graphorin.provider.model"
31
+ ]);
32
+ if (provider === null || model === null) continue;
33
+ if (lookupPrice({
34
+ provider,
35
+ model
36
+ }, snapshot) !== null) continue;
37
+ const key = `${provider}/${model}`;
38
+ const existing = seen.get(key);
39
+ if (existing === void 0) seen.set(key, {
40
+ provider,
41
+ model,
42
+ count: 1
43
+ });
44
+ else seen.set(key, {
45
+ provider,
46
+ model,
47
+ count: existing.count + 1
48
+ });
49
+ }
50
+ return [...seen.values()].sort((a, b) => b.count - a.count);
51
+ }
52
+ function stringAttr(span, keys) {
53
+ for (const key of keys) {
54
+ const value = span.attributes[key];
55
+ if (typeof value === "string" && value.length > 0) return value;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ //#endregion
61
+ export { listMissingModels };
62
+ //# sourceMappingURL=missing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing.js","names":[],"sources":["../src/missing.ts"],"sourcesContent":["/**\n * `listMissingModels(...)` — scan a sequence of trace spans and return\n * the unique (provider, model) pairs that the snapshot does not\n * recognise.\n *\n * The function reads `gen_ai.system` + `gen_ai.request.model` (the\n * canonical OpenTelemetry GenAI attributes) and falls back to the\n * Graphorin-prefixed `graphorin.provider.id` /\n * `graphorin.provider.model` pair when `gen_ai.*` is absent.\n *\n * @packageDocumentation\n */\n\nimport { lookupPrice } from './lookup.js';\nimport { BUNDLED_SNAPSHOT } from './snapshot/bundled.js';\nimport type { PricingSnapshot, PricingTraceSpanLike } from './types.js';\n\n/**\n * Single missing-model row.\n *\n * @stable\n */\nexport interface MissingModelEntry {\n readonly provider: string;\n readonly model: string;\n readonly count: number;\n}\n\n/**\n * Return one entry per (provider, model) pair that the snapshot does\n * not recognise, sorted by descending occurrence count.\n *\n * @stable\n */\nexport function listMissingModels(\n spans: Iterable<PricingTraceSpanLike>,\n snapshot: PricingSnapshot = BUNDLED_SNAPSHOT,\n): ReadonlyArray<MissingModelEntry> {\n const seen = new Map<string, MissingModelEntry & { count: number }>();\n for (const span of spans) {\n const provider = stringAttr(span, ['gen_ai.system', 'graphorin.provider.id']);\n const model = stringAttr(span, [\n 'gen_ai.request.model',\n 'gen_ai.response.model',\n 'graphorin.provider.model',\n ]);\n if (provider === null || model === null) continue;\n if (lookupPrice({ provider, model }, snapshot) !== null) continue;\n const key = `${provider}/${model}`;\n const existing = seen.get(key);\n if (existing === undefined) {\n seen.set(key, { provider, model, count: 1 });\n } else {\n seen.set(key, { provider, model, count: existing.count + 1 });\n }\n }\n return [...seen.values()].sort((a, b) => b.count - a.count);\n}\n\nfunction stringAttr(span: PricingTraceSpanLike, keys: ReadonlyArray<string>): string | null {\n for (const key of keys) {\n const value = span.attributes[key];\n if (typeof value === 'string' && value.length > 0) return value;\n }\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,kBACd,OACA,WAA4B,kBACM;CAClC,MAAM,uBAAO,IAAI,KAAoD;AACrE,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,WAAW,WAAW,MAAM,CAAC,iBAAiB,wBAAwB,CAAC;EAC7E,MAAM,QAAQ,WAAW,MAAM;GAC7B;GACA;GACA;GACD,CAAC;AACF,MAAI,aAAa,QAAQ,UAAU,KAAM;AACzC,MAAI,YAAY;GAAE;GAAU;GAAO,EAAE,SAAS,KAAK,KAAM;EACzD,MAAM,MAAM,GAAG,SAAS,GAAG;EAC3B,MAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,MAAI,aAAa,OACf,MAAK,IAAI,KAAK;GAAE;GAAU;GAAO,OAAO;GAAG,CAAC;MAE5C,MAAK,IAAI,KAAK;GAAE;GAAU;GAAO,OAAO,SAAS,QAAQ;GAAG,CAAC;;AAGjE,QAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;;AAG7D,SAAS,WAAW,MAA4B,MAA4C;AAC1F,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,KAAK,WAAW;AAC9B,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,QAAO;;AAE5D,QAAO"}
@@ -0,0 +1,44 @@
1
+ import { PricingSnapshot } from "./types.js";
2
+
3
+ //#region src/refresh.d.ts
4
+
5
+ /**
6
+ * Configuration shape for {@link refreshPricing}. The `fetchImpl`
7
+ * override exists so tests can exercise the function without making
8
+ * a real network call.
9
+ *
10
+ * @stable
11
+ */
12
+ interface RefreshPricingOptions {
13
+ /** Snapshot URL — typically the upstream pricing JSON. */
14
+ readonly url: string;
15
+ /** Optional headers (auth, conditional GET, etc.). */
16
+ readonly headers?: Readonly<Record<string, string>>;
17
+ /** Optional fetch override — useful in tests. */
18
+ readonly fetchImpl?: typeof fetch;
19
+ /** Override the snapshot date stamped on the result. Defaults to today. */
20
+ readonly snapshotDate?: string;
21
+ /** Override the snapshot version string. Defaults to `'graphorin/0.1+refreshed'`. */
22
+ readonly version?: string;
23
+ /**
24
+ * Hard timeout for the network fetch in milliseconds. Default
25
+ * `30000`. Aborts the request (and throws) if the upstream is slow
26
+ * or unreachable so `graphorin pricing refresh` cannot hang. Pass an
27
+ * explicit {@link RefreshPricingOptions.signal} to manage
28
+ * cancellation yourself; the two are combined.
29
+ */
30
+ readonly timeoutMs?: number;
31
+ /** Caller-supplied abort signal, combined with the timeout. */
32
+ readonly signal?: AbortSignal;
33
+ }
34
+ /**
35
+ * Pull a fresh snapshot from the supplied URL and return it. Network
36
+ * failures and shape mismatches surface as thrown errors so the CLI
37
+ * can surface them to the operator.
38
+ *
39
+ * @stable
40
+ */
41
+ declare function refreshPricing(opts: RefreshPricingOptions): Promise<PricingSnapshot>;
42
+ //#endregion
43
+ export { RefreshPricingOptions, refreshPricing };
44
+ //# sourceMappingURL=refresh.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,83 @@
1
+ import { computeEntriesDigest } from "./snapshot/bundled.js";
2
+
3
+ //#region src/refresh.ts
4
+ /**
5
+ * `refreshPricing(...)` — opt-in network call. The framework never
6
+ * invokes this automatically; it exists so the CLI (`graphorin
7
+ * pricing refresh`) and operator scripts can pull a fresh snapshot
8
+ * without re-installing the package.
9
+ *
10
+ * The function fetches the supplied URL as JSON and validates the
11
+ * shape against the {@link PricingSnapshot} contract. The bundled
12
+ * snapshot is **not** mutated; callers persist the refreshed
13
+ * snapshot to disk and reload it explicitly.
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+ /**
18
+ * Pull a fresh snapshot from the supplied URL and return it. Network
19
+ * failures and shape mismatches surface as thrown errors so the CLI
20
+ * can surface them to the operator.
21
+ *
22
+ * @stable
23
+ */
24
+ async function refreshPricing(opts) {
25
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
26
+ if (typeof fetchImpl !== "function") throw new TypeError("refreshPricing: no fetch implementation available. Provide `fetchImpl` explicitly or run on a Node.js version with global fetch (>= 18).");
27
+ const timeoutMs = opts.timeoutMs ?? 3e4;
28
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
29
+ const signal = opts.signal === void 0 ? timeoutSignal : AbortSignal.any([opts.signal, timeoutSignal]);
30
+ let res;
31
+ try {
32
+ res = await fetchImpl(opts.url, {
33
+ headers: opts.headers ?? {},
34
+ signal
35
+ });
36
+ } catch (err) {
37
+ if (timeoutSignal.aborted) throw new Error(`refreshPricing: timed out after ${timeoutMs} ms fetching ${opts.url}`, { cause: err });
38
+ throw err;
39
+ }
40
+ if (!res.ok) throw new Error(`refreshPricing: HTTP ${res.status} ${res.statusText} from ${opts.url}`);
41
+ const entries = parseEntries(await res.json());
42
+ const snapshotDate = opts.snapshotDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
43
+ const version = opts.version ?? "graphorin/0.1+refreshed";
44
+ return Object.freeze({
45
+ version,
46
+ source: opts.url,
47
+ snapshotDate,
48
+ currency: "USD",
49
+ sha256: computeEntriesDigest(entries),
50
+ entries
51
+ });
52
+ }
53
+ function parseEntries(body) {
54
+ if (body === null || typeof body !== "object") throw new Error("refreshPricing: response body is not an object");
55
+ let raw;
56
+ if (Array.isArray(body)) raw = body;
57
+ else {
58
+ raw = body.entries;
59
+ if (!Array.isArray(raw)) throw new Error("refreshPricing: response object is missing the `entries` array");
60
+ }
61
+ const entries = raw.map(parseEntry);
62
+ return Object.freeze(entries);
63
+ }
64
+ function parseEntry(raw) {
65
+ if (raw === null || typeof raw !== "object") throw new Error("refreshPricing: pricing entry is not an object");
66
+ const e = raw;
67
+ if (typeof e.provider !== "string" || typeof e.model !== "string") throw new Error("refreshPricing: pricing entry is missing provider / model");
68
+ if (typeof e.inputUsdPerToken !== "number" || typeof e.outputUsdPerToken !== "number") throw new Error(`refreshPricing: pricing entry ${e.provider}/${e.model} is missing input / output prices`);
69
+ return Object.freeze({
70
+ provider: e.provider,
71
+ model: e.model,
72
+ inputUsdPerToken: e.inputUsdPerToken,
73
+ outputUsdPerToken: e.outputUsdPerToken,
74
+ ...typeof e.cachedReadUsdPerToken === "number" ? { cachedReadUsdPerToken: e.cachedReadUsdPerToken } : {},
75
+ ...typeof e.reasoningUsdPerToken === "number" ? { reasoningUsdPerToken: e.reasoningUsdPerToken } : {},
76
+ ...typeof e.region === "string" ? { region: e.region } : {},
77
+ ...typeof e.notes === "string" ? { notes: e.notes } : {}
78
+ });
79
+ }
80
+
81
+ //#endregion
82
+ export { refreshPricing };
83
+ //# sourceMappingURL=refresh.js.map
@@ -0,0 +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.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,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"}
@@ -0,0 +1,25 @@
1
+ import { ModelPrice, PricingSnapshot } from "../types.js";
2
+
3
+ //#region src/snapshot/bundled.d.ts
4
+
5
+ /** @internal — used for `lookupPrice` defaults. */
6
+ declare const SNAPSHOT_DATE = "2026-04-30";
7
+ /**
8
+ * Compute a deterministic SHA-256 of the entries. Sorting the entry
9
+ * keys before serialisation keeps the digest stable across Node
10
+ * versions / object-property-iteration orderings.
11
+ *
12
+ * @internal
13
+ */
14
+ declare function computeEntriesDigest(entries: ReadonlyArray<ModelPrice>): string;
15
+ /**
16
+ * The bundled snapshot. The `sha256` digest is computed over the
17
+ * canonical JSON form of `entries` at module load time so consumers
18
+ * can verify integrity without trusting the package metadata.
19
+ *
20
+ * @stable
21
+ */
22
+ declare const BUNDLED_SNAPSHOT: PricingSnapshot;
23
+ //#endregion
24
+ export { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest };
25
+ //# sourceMappingURL=bundled.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled.d.ts","names":[],"sources":["../../src/snapshot/bundled.ts"],"sourcesContent":[],"mappings":";;;;;cAkBa,aAAA;;;;;;;;iBAoIG,oBAAA,UAA8B,cAAc;;;;;;;;cAsB/C,kBAAkB"}
@@ -0,0 +1,157 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ //#region src/snapshot/bundled.ts
4
+ /**
5
+ * Bundled pricing snapshot. The catalogue is intentionally small —
6
+ * operators wanting the full upstream catalogue should run the opt-in
7
+ * `refreshPricing(...)` flow at build time and persist the result to
8
+ * a custom snapshot file.
9
+ *
10
+ * Prices are stored in **USD per token** (i.e. divide the per-million
11
+ * upstream prices by 1_000_000). Numbers are accurate to the snapshot
12
+ * date; consumers are expected to refresh on a regular cadence.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ /** @internal — used for `lookupPrice` defaults. */
17
+ const SNAPSHOT_DATE = "2026-04-30";
18
+ const ENTRIES = Object.freeze([
19
+ {
20
+ provider: "anthropic",
21
+ model: "claude-3-5-sonnet-20241022",
22
+ inputUsdPerToken: 3 / 1e6,
23
+ outputUsdPerToken: 15 / 1e6,
24
+ cachedReadUsdPerToken: .3 / 1e6
25
+ },
26
+ {
27
+ provider: "anthropic",
28
+ model: "claude-3-5-haiku-20241022",
29
+ inputUsdPerToken: .8 / 1e6,
30
+ outputUsdPerToken: 4 / 1e6,
31
+ cachedReadUsdPerToken: .08 / 1e6
32
+ },
33
+ {
34
+ provider: "anthropic",
35
+ model: "claude-3-opus-20240229",
36
+ inputUsdPerToken: 15 / 1e6,
37
+ outputUsdPerToken: 75 / 1e6,
38
+ cachedReadUsdPerToken: 1.5 / 1e6
39
+ },
40
+ {
41
+ provider: "openai",
42
+ model: "gpt-4o-2024-11-20",
43
+ inputUsdPerToken: 2.5 / 1e6,
44
+ outputUsdPerToken: 10 / 1e6,
45
+ cachedReadUsdPerToken: 1.25 / 1e6
46
+ },
47
+ {
48
+ provider: "openai",
49
+ model: "gpt-4o-mini-2024-07-18",
50
+ inputUsdPerToken: .15 / 1e6,
51
+ outputUsdPerToken: .6 / 1e6,
52
+ cachedReadUsdPerToken: .075 / 1e6
53
+ },
54
+ {
55
+ provider: "openai",
56
+ model: "o1-2024-12-17",
57
+ inputUsdPerToken: 15 / 1e6,
58
+ outputUsdPerToken: 60 / 1e6,
59
+ cachedReadUsdPerToken: 7.5 / 1e6
60
+ },
61
+ {
62
+ provider: "openai",
63
+ model: "o3-mini-2025-01-31",
64
+ inputUsdPerToken: 1.1 / 1e6,
65
+ outputUsdPerToken: 4.4 / 1e6,
66
+ cachedReadUsdPerToken: .55 / 1e6
67
+ },
68
+ {
69
+ provider: "google",
70
+ model: "gemini-1.5-pro-002",
71
+ inputUsdPerToken: 1.25 / 1e6,
72
+ outputUsdPerToken: 5 / 1e6,
73
+ cachedReadUsdPerToken: .3125 / 1e6
74
+ },
75
+ {
76
+ provider: "google",
77
+ model: "gemini-1.5-flash-002",
78
+ inputUsdPerToken: .075 / 1e6,
79
+ outputUsdPerToken: .3 / 1e6,
80
+ cachedReadUsdPerToken: .01875 / 1e6
81
+ },
82
+ {
83
+ provider: "mistral",
84
+ model: "mistral-large-2411",
85
+ inputUsdPerToken: 2 / 1e6,
86
+ outputUsdPerToken: 6 / 1e6
87
+ },
88
+ {
89
+ provider: "mistral",
90
+ model: "mistral-small-2503",
91
+ inputUsdPerToken: .2 / 1e6,
92
+ outputUsdPerToken: .6 / 1e6
93
+ },
94
+ {
95
+ provider: "cohere",
96
+ model: "command-r-plus-08-2024",
97
+ inputUsdPerToken: 2.5 / 1e6,
98
+ outputUsdPerToken: 10 / 1e6
99
+ },
100
+ {
101
+ provider: "cohere",
102
+ model: "command-r-08-2024",
103
+ inputUsdPerToken: .15 / 1e6,
104
+ outputUsdPerToken: .6 / 1e6
105
+ },
106
+ {
107
+ provider: "ollama",
108
+ model: "*",
109
+ inputUsdPerToken: 0,
110
+ outputUsdPerToken: 0,
111
+ notes: "Local Ollama deployment — operator-priced."
112
+ },
113
+ {
114
+ provider: "graphorin-llamacpp",
115
+ model: "*",
116
+ inputUsdPerToken: 0,
117
+ outputUsdPerToken: 0,
118
+ notes: "Local llama.cpp deployment — operator-priced."
119
+ }
120
+ ]);
121
+ /**
122
+ * Compute a deterministic SHA-256 of the entries. Sorting the entry
123
+ * keys before serialisation keeps the digest stable across Node
124
+ * versions / object-property-iteration orderings.
125
+ *
126
+ * @internal
127
+ */
128
+ function computeEntriesDigest(entries) {
129
+ const canonical = JSON.stringify(entries.map((entry) => sortKeys(entry)));
130
+ return createHash("sha256").update(canonical, "utf8").digest("hex");
131
+ }
132
+ function sortKeys(value) {
133
+ if (value === null || typeof value !== "object") return value;
134
+ if (Array.isArray(value)) return value.map(sortKeys);
135
+ const out = {};
136
+ for (const key of Object.keys(value).sort()) out[key] = sortKeys(value[key]);
137
+ return out;
138
+ }
139
+ /**
140
+ * The bundled snapshot. The `sha256` digest is computed over the
141
+ * canonical JSON form of `entries` at module load time so consumers
142
+ * can verify integrity without trusting the package metadata.
143
+ *
144
+ * @stable
145
+ */
146
+ const BUNDLED_SNAPSHOT = Object.freeze({
147
+ version: "graphorin/0.1",
148
+ source: "https://github.com/o-stepper/graphorin/tree/main/packages/pricing/src/snapshot/bundled.ts",
149
+ snapshotDate: SNAPSHOT_DATE,
150
+ currency: "USD",
151
+ sha256: computeEntriesDigest(ENTRIES),
152
+ entries: ENTRIES
153
+ });
154
+
155
+ //#endregion
156
+ export { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest };
157
+ //# sourceMappingURL=bundled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled.js","names":["ENTRIES: ReadonlyArray<ModelPrice>","out: Record<string, unknown>","BUNDLED_SNAPSHOT: PricingSnapshot"],"sources":["../../src/snapshot/bundled.ts"],"sourcesContent":["/**\n * Bundled pricing snapshot. The catalogue is intentionally small —\n * operators wanting the full upstream catalogue should run the opt-in\n * `refreshPricing(...)` flow at build time and persist the result to\n * a custom snapshot file.\n *\n * Prices are stored in **USD per token** (i.e. divide the per-million\n * upstream prices by 1_000_000). Numbers are accurate to the snapshot\n * date; consumers are expected to refresh on a regular cadence.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\n\nimport type { ModelPrice, PricingSnapshot } from '../types.js';\n\n/** @internal — used for `lookupPrice` defaults. */\nexport const SNAPSHOT_DATE = '2026-04-30';\n\nconst ENTRIES: ReadonlyArray<ModelPrice> = Object.freeze([\n // -----------------------------------------------------------------\n // Anthropic\n // -----------------------------------------------------------------\n {\n provider: 'anthropic',\n model: 'claude-3-5-sonnet-20241022',\n inputUsdPerToken: 3 / 1_000_000,\n outputUsdPerToken: 15 / 1_000_000,\n cachedReadUsdPerToken: 0.3 / 1_000_000,\n },\n {\n provider: 'anthropic',\n model: 'claude-3-5-haiku-20241022',\n inputUsdPerToken: 0.8 / 1_000_000,\n outputUsdPerToken: 4 / 1_000_000,\n cachedReadUsdPerToken: 0.08 / 1_000_000,\n },\n {\n provider: 'anthropic',\n model: 'claude-3-opus-20240229',\n inputUsdPerToken: 15 / 1_000_000,\n outputUsdPerToken: 75 / 1_000_000,\n cachedReadUsdPerToken: 1.5 / 1_000_000,\n },\n // -----------------------------------------------------------------\n // OpenAI\n // -----------------------------------------------------------------\n {\n provider: 'openai',\n model: 'gpt-4o-2024-11-20',\n inputUsdPerToken: 2.5 / 1_000_000,\n outputUsdPerToken: 10 / 1_000_000,\n cachedReadUsdPerToken: 1.25 / 1_000_000,\n },\n {\n provider: 'openai',\n model: 'gpt-4o-mini-2024-07-18',\n inputUsdPerToken: 0.15 / 1_000_000,\n outputUsdPerToken: 0.6 / 1_000_000,\n cachedReadUsdPerToken: 0.075 / 1_000_000,\n },\n {\n provider: 'openai',\n model: 'o1-2024-12-17',\n inputUsdPerToken: 15 / 1_000_000,\n outputUsdPerToken: 60 / 1_000_000,\n cachedReadUsdPerToken: 7.5 / 1_000_000,\n },\n {\n provider: 'openai',\n model: 'o3-mini-2025-01-31',\n inputUsdPerToken: 1.1 / 1_000_000,\n outputUsdPerToken: 4.4 / 1_000_000,\n cachedReadUsdPerToken: 0.55 / 1_000_000,\n },\n // -----------------------------------------------------------------\n // Google\n // -----------------------------------------------------------------\n {\n provider: 'google',\n model: 'gemini-1.5-pro-002',\n inputUsdPerToken: 1.25 / 1_000_000,\n outputUsdPerToken: 5 / 1_000_000,\n cachedReadUsdPerToken: 0.3125 / 1_000_000,\n },\n {\n provider: 'google',\n model: 'gemini-1.5-flash-002',\n inputUsdPerToken: 0.075 / 1_000_000,\n outputUsdPerToken: 0.3 / 1_000_000,\n cachedReadUsdPerToken: 0.01875 / 1_000_000,\n },\n // -----------------------------------------------------------------\n // Mistral\n // -----------------------------------------------------------------\n {\n provider: 'mistral',\n model: 'mistral-large-2411',\n inputUsdPerToken: 2 / 1_000_000,\n outputUsdPerToken: 6 / 1_000_000,\n },\n {\n provider: 'mistral',\n model: 'mistral-small-2503',\n inputUsdPerToken: 0.2 / 1_000_000,\n outputUsdPerToken: 0.6 / 1_000_000,\n },\n // -----------------------------------------------------------------\n // Cohere\n // -----------------------------------------------------------------\n {\n provider: 'cohere',\n model: 'command-r-plus-08-2024',\n inputUsdPerToken: 2.5 / 1_000_000,\n outputUsdPerToken: 10 / 1_000_000,\n },\n {\n provider: 'cohere',\n model: 'command-r-08-2024',\n inputUsdPerToken: 0.15 / 1_000_000,\n outputUsdPerToken: 0.6 / 1_000_000,\n },\n // -----------------------------------------------------------------\n // Local providers — documented as zero-cost so cost rollups still\n // include the call counts.\n // -----------------------------------------------------------------\n {\n provider: 'ollama',\n model: '*',\n inputUsdPerToken: 0,\n outputUsdPerToken: 0,\n notes: 'Local Ollama deployment — operator-priced.',\n },\n {\n provider: 'graphorin-llamacpp',\n model: '*',\n inputUsdPerToken: 0,\n outputUsdPerToken: 0,\n notes: 'Local llama.cpp deployment — operator-priced.',\n },\n] as const);\n\n/**\n * Compute a deterministic SHA-256 of the entries. Sorting the entry\n * keys before serialisation keeps the digest stable across Node\n * versions / object-property-iteration orderings.\n *\n * @internal\n */\nexport function computeEntriesDigest(entries: ReadonlyArray<ModelPrice>): string {\n const canonical = JSON.stringify(entries.map((entry) => sortKeys(entry as unknown)));\n return createHash('sha256').update(canonical, 'utf8').digest('hex');\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (value === null || typeof value !== 'object') return value;\n if (Array.isArray(value)) return value.map(sortKeys);\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n out[key] = sortKeys((value as Record<string, unknown>)[key]);\n }\n return out;\n}\n\n/**\n * The bundled snapshot. The `sha256` digest is computed over the\n * canonical JSON form of `entries` at module load time so consumers\n * can verify integrity without trusting the package metadata.\n *\n * @stable\n */\nexport const BUNDLED_SNAPSHOT: PricingSnapshot = Object.freeze({\n version: 'graphorin/0.1',\n source:\n 'https://github.com/o-stepper/graphorin/tree/main/packages/pricing/src/snapshot/bundled.ts',\n snapshotDate: SNAPSHOT_DATE,\n currency: 'USD',\n sha256: computeEntriesDigest(ENTRIES),\n entries: ENTRIES,\n});\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAa,gBAAgB;AAE7B,MAAMA,UAAqC,OAAO,OAAO;CAIvD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,IAAI;EACtB,mBAAmB,KAAK;EACxB,uBAAuB,KAAM;EAC9B;CACD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,KAAM;EACxB,mBAAmB,IAAI;EACvB,uBAAuB,MAAO;EAC/B;CACD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,KAAK;EACvB,mBAAmB,KAAK;EACxB,uBAAuB,MAAM;EAC9B;CAID;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,MAAM;EACxB,mBAAmB,KAAK;EACxB,uBAAuB,OAAO;EAC/B;CACD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,MAAO;EACzB,mBAAmB,KAAM;EACzB,uBAAuB,OAAQ;EAChC;CACD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,KAAK;EACvB,mBAAmB,KAAK;EACxB,uBAAuB,MAAM;EAC9B;CACD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,MAAM;EACxB,mBAAmB,MAAM;EACzB,uBAAuB,MAAO;EAC/B;CAID;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,OAAO;EACzB,mBAAmB,IAAI;EACvB,uBAAuB,QAAS;EACjC;CACD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,OAAQ;EAC1B,mBAAmB,KAAM;EACzB,uBAAuB,SAAU;EAClC;CAID;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,IAAI;EACtB,mBAAmB,IAAI;EACxB;CACD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,KAAM;EACxB,mBAAmB,KAAM;EAC1B;CAID;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,MAAM;EACxB,mBAAmB,KAAK;EACzB;CACD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB,MAAO;EACzB,mBAAmB,KAAM;EAC1B;CAKD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB;EAClB,mBAAmB;EACnB,OAAO;EACR;CACD;EACE,UAAU;EACV,OAAO;EACP,kBAAkB;EAClB,mBAAmB;EACnB,OAAO;EACR;CACF,CAAU;;;;;;;;AASX,SAAgB,qBAAqB,SAA4C;CAC/E,MAAM,YAAY,KAAK,UAAU,QAAQ,KAAK,UAAU,SAAS,MAAiB,CAAC,CAAC;AACpF,QAAO,WAAW,SAAS,CAAC,OAAO,WAAW,OAAO,CAAC,OAAO,MAAM;;AAGrE,SAAS,SAAS,OAAyB;AACzC,KAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,MAAM,IAAI,SAAS;CACpD,MAAMC,MAA+B,EAAE;AACvC,MAAK,MAAM,OAAO,OAAO,KAAK,MAAiC,CAAC,MAAM,CACpE,KAAI,OAAO,SAAU,MAAkC,KAAK;AAE9D,QAAO;;;;;;;;;AAUT,MAAaC,mBAAoC,OAAO,OAAO;CAC7D,SAAS;CACT,QACE;CACF,cAAc;CACd,UAAU;CACV,QAAQ,qBAAqB,QAAQ;CACrC,SAAS;CACV,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest } from "./bundled.js";
2
+ export { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest };
@@ -0,0 +1,3 @@
1
+ import { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest } from "./bundled.js";
2
+
3
+ export { BUNDLED_SNAPSHOT, SNAPSHOT_DATE, computeEntriesDigest };
@@ -0,0 +1,92 @@
1
+ import { Cost } from "@graphorin/core";
2
+
3
+ //#region src/types.d.ts
4
+
5
+ /**
6
+ * Per-model pricing entry. All amounts are in **USD per token** unless
7
+ * the snapshot declares an alternative currency. Reasoning tokens
8
+ * (when supported) follow the same pricing as completion tokens unless
9
+ * the entry declares an explicit `reasoningUsdPerToken`.
10
+ *
11
+ * @stable
12
+ */
13
+ interface ModelPrice {
14
+ /** Lower-case provider id (e.g. `'anthropic'`, `'openai'`, `'ollama'`). */
15
+ readonly provider: string;
16
+ /** Lower-case model id, e.g. `'claude-3-5-sonnet-20241022'`. */
17
+ readonly model: string;
18
+ /** Price per input token, in USD. */
19
+ readonly inputUsdPerToken: number;
20
+ /** Price per output token, in USD. */
21
+ readonly outputUsdPerToken: number;
22
+ /** Optional cached-read price (Anthropic / OpenAI prompt caching). */
23
+ readonly cachedReadUsdPerToken?: number;
24
+ /** Optional reasoning-token price (OpenAI o1 / Gemini 2 thinking). */
25
+ readonly reasoningUsdPerToken?: number;
26
+ /** Optional region label (e.g. `'us-east-1'`). */
27
+ readonly region?: string;
28
+ /** Free-form notes for tooling / docs. */
29
+ readonly notes?: string;
30
+ }
31
+ /**
32
+ * Single bundled snapshot.
33
+ *
34
+ * @stable
35
+ */
36
+ interface PricingSnapshot {
37
+ readonly version: string;
38
+ readonly source: string;
39
+ readonly snapshotDate: string;
40
+ readonly currency: 'USD';
41
+ readonly sha256: string;
42
+ readonly entries: ReadonlyArray<ModelPrice>;
43
+ }
44
+ /**
45
+ * Lookup criteria.
46
+ *
47
+ * @stable
48
+ */
49
+ interface LookupPriceArgs {
50
+ readonly provider: string;
51
+ readonly model: string;
52
+ readonly region?: string;
53
+ }
54
+ /**
55
+ * Result of {@link lookupPrice} when the model is known.
56
+ *
57
+ * @stable
58
+ */
59
+ interface LookupPriceResult {
60
+ readonly inputUsdPerToken: number;
61
+ readonly outputUsdPerToken: number;
62
+ readonly cachedReadUsdPerToken?: number;
63
+ readonly reasoningUsdPerToken?: number;
64
+ readonly source: string;
65
+ readonly snapshotDate: string;
66
+ }
67
+ /**
68
+ * Result row reported by {@link diffPricing}.
69
+ *
70
+ * @stable
71
+ */
72
+ interface PricingDiffEntry {
73
+ readonly provider: string;
74
+ readonly model: string;
75
+ readonly kind: 'added' | 'removed' | 'changed';
76
+ readonly before?: ModelPrice;
77
+ readonly after?: ModelPrice;
78
+ readonly changedFields?: ReadonlyArray<keyof ModelPrice>;
79
+ }
80
+ /**
81
+ * Span-shape input accepted by {@link listMissingModels}. Lightweight
82
+ * subset of `SpanRecord` from `@graphorin/observability` so the
83
+ * pricing package stays free of an observability dependency.
84
+ *
85
+ * @stable
86
+ */
87
+ interface PricingTraceSpanLike {
88
+ readonly attributes: Readonly<Record<string, unknown>>;
89
+ }
90
+ //#endregion
91
+ export { type Cost, LookupPriceArgs, LookupPriceResult, ModelPrice, PricingDiffEntry, PricingSnapshot, PricingTraceSpanLike };
92
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;AAiEA;AAcA;;;;;;AAgBA;UA/EiB,UAAA;;;;;;;;;;;;;;;;;;;;;;;UAwBA,eAAA;;;;;;oBAMG,cAAc;;;;;;;UAQjB,eAAA;;;;;;;;;;UAWA,iBAAA;;;;;;;;;;;;;UAcA,gBAAA;;;;oBAIG;mBACD;2BACQ,oBAAoB;;;;;;;;;UAU9B,oBAAA;uBACM,SAAS"}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@graphorin/pricing",
3
+ "version": "0.5.0",
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
+ "license": "MIT",
6
+ "author": "Oleksiy Stepurenko",
7
+ "homepage": "https://github.com/o-stepper/graphorin/tree/main/packages/pricing",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/o-stepper/graphorin.git",
11
+ "directory": "packages/pricing"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/o-stepper/graphorin/issues"
15
+ },
16
+ "keywords": [
17
+ "graphorin",
18
+ "ai",
19
+ "agents",
20
+ "framework",
21
+ "pricing",
22
+ "tokens",
23
+ "cost",
24
+ "llm"
25
+ ],
26
+ "type": "module",
27
+ "engines": {
28
+ "node": ">=22.0.0"
29
+ },
30
+ "main": "./dist/index.js",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ },
38
+ "./snapshot": {
39
+ "types": "./dist/snapshot/index.d.ts",
40
+ "import": "./dist/snapshot/index.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md",
47
+ "CHANGELOG.md",
48
+ "LICENSE"
49
+ ],
50
+ "dependencies": {
51
+ "@graphorin/core": "0.5.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "provenance": true
56
+ },
57
+ "devDependencies": {},
58
+ "scripts": {
59
+ "build": "tsdown",
60
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
61
+ "test": "vitest run",
62
+ "lint": "biome check .",
63
+ "clean": "rimraf dist .turbo *.tsbuildinfo"
64
+ }
65
+ }