sidekick-shared 0.28.3 → 0.29.1
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 +86 -0
- package/README.md +188 -27
- package/dist/aggregation/EventAggregator.d.ts +19 -0
- package/dist/aggregation/EventAggregator.js +174 -35
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +13 -0
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +16 -6
- package/dist/browser.js +24 -2
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/context/sessionContext.js +4 -0
- package/dist/esm/aggregation/EventAggregator.js +174 -35
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +5 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/context/sessionContext.js +4 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +11 -6
- package/dist/esm/modelCatalog.js +7 -5
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +425 -47
- package/dist/esm/node.js +9 -3
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +351 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +85 -22
- package/dist/esm/parsers/subagentScanner.js +33 -12
- package/dist/esm/parsers/subagentTraceParser.js +97 -37
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +17 -9
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/claudeCode.js +10 -2
- package/dist/esm/providers/codex.js +84 -62
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/sessionEvent.js +35 -14
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/subagentSpawn.js +163 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +46 -3
- package/dist/esm/turns/assistantTurn.js +3 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/eventBridge.js +15 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +442 -58
- package/dist/esm/watchers/jsonlWatcher.js +51 -4
- package/dist/esm/watchers/providerReaderWatcher.js +13 -0
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +22 -11
- package/dist/index.js +44 -12
- package/dist/modelCatalog.js +7 -5
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +434 -49
- package/dist/node.d.ts +16 -5
- package/dist/node.js +33 -2
- package/dist/observedContextWindows.js +2 -1
- package/dist/ownKey.d.ts +16 -0
- package/dist/ownKey.js +30 -0
- package/dist/parsers/codexParser.d.ts +118 -0
- package/dist/parsers/codexParser.js +352 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +88 -22
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +33 -12
- package/dist/parsers/subagentTraceParser.d.ts +25 -5
- package/dist/parsers/subagentTraceParser.js +97 -37
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +16 -8
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/claudeCode.js +9 -1
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +85 -64
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/providers/types.d.ts +14 -0
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/sessionEvent.d.ts +27 -10
- package/dist/schemas/sessionEvent.js +35 -14
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/subagentSpawn.d.ts +80 -0
- package/dist/subagentSpawn.js +174 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +46 -3
- package/dist/turns/assistantTurn.d.ts +1 -0
- package/dist/turns/assistantTurn.js +3 -2
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/eventBridge.js +15 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +5 -3
- package/dist/watchers/jsonlTail.d.ts +124 -4
- package/dist/watchers/jsonlTail.js +446 -59
- package/dist/watchers/jsonlWatcher.d.ts +18 -1
- package/dist/watchers/jsonlWatcher.js +50 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +13 -0
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +41 -1
package/dist/modelInfo.js
CHANGED
|
@@ -15,17 +15,26 @@
|
|
|
15
15
|
* baseline without requiring a redeploy when vendor prices change.
|
|
16
16
|
* - Longest-prefix matching handles variants like `claude-opus-4.5-20250514`
|
|
17
17
|
* against a key like `claude-opus-4.5`. Same pattern used in modelContext.ts.
|
|
18
|
+
* - A consumer layer (`registerModelPricing`) sits beside the catalog and
|
|
19
|
+
* imported layers and survives their resets.
|
|
20
|
+
* - Every table-derived value is computed lazily, so a tree-shaken bundle that
|
|
21
|
+
* only uses the parsing/display helpers does not retain the pricing table.
|
|
18
22
|
*
|
|
19
23
|
* @module modelInfo
|
|
20
24
|
*/
|
|
21
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.MODEL_PROVIDER_IDS = void 0;
|
|
26
|
+
exports.formatCost = exports.MODEL_PROVIDER_IDS = void 0;
|
|
23
27
|
exports._setPricingOverrides = _setPricingOverrides;
|
|
24
28
|
exports._getPricingOverrides = _getPricingOverrides;
|
|
25
29
|
exports._clearPricingOverrides = _clearPricingOverrides;
|
|
26
30
|
exports._setImportedResolvedPricing = _setImportedResolvedPricing;
|
|
27
31
|
exports._clearImportedResolvedPricing = _clearImportedResolvedPricing;
|
|
32
|
+
exports.registerModelPricing = registerModelPricing;
|
|
33
|
+
exports.clearRegisteredModelPricing = clearRegisteredModelPricing;
|
|
34
|
+
exports.getPricingCatalogInfo = getPricingCatalogInfo;
|
|
28
35
|
exports._getModelPricingIds = _getModelPricingIds;
|
|
36
|
+
exports.canonicalModelId = canonicalModelId;
|
|
37
|
+
exports.isMovingModelAlias = isMovingModelAlias;
|
|
29
38
|
exports.parseModelId = parseModelId;
|
|
30
39
|
exports.resolveModelPricing = resolveModelPricing;
|
|
31
40
|
exports.getModelPricing = getModelPricing;
|
|
@@ -34,15 +43,19 @@ exports.calculateCostWithPricing = calculateCostWithPricing;
|
|
|
34
43
|
exports.calculateCost = calculateCost;
|
|
35
44
|
exports.calculateCostWithProvenance = calculateCostWithProvenance;
|
|
36
45
|
exports.mergeCostSources = mergeCostSources;
|
|
46
|
+
exports.costSourceFromPricingProvenance = costSourceFromPricingProvenance;
|
|
47
|
+
exports.mergeCostComponents = mergeCostComponents;
|
|
37
48
|
exports.shortModelName = shortModelName;
|
|
38
49
|
exports.getModelDisplayInfo = getModelDisplayInfo;
|
|
39
50
|
exports.compareModelIds = compareModelIds;
|
|
40
51
|
exports.sortModelIds = sortModelIds;
|
|
41
|
-
exports.formatCost = formatCost;
|
|
42
52
|
const modelContext_1 = require("./modelContext");
|
|
43
53
|
const modelAliases_1 = require("./modelAliases");
|
|
54
|
+
const ownKey_1 = require("./ownKey");
|
|
44
55
|
var providerIds_1 = require("./providerIds");
|
|
45
56
|
Object.defineProperty(exports, "MODEL_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.MODEL_PROVIDER_IDS; } });
|
|
57
|
+
var formatting_1 = require("./formatting");
|
|
58
|
+
Object.defineProperty(exports, "formatCost", { enumerable: true, get: function () { return formatting_1.formatCost; } });
|
|
46
59
|
// ── Static Pricing Table ──
|
|
47
60
|
/**
|
|
48
61
|
* Static baseline pricing. Keys are model-ID prefixes; lookup uses
|
|
@@ -644,21 +657,38 @@ const PRICING_TABLE = {
|
|
|
644
657
|
cacheReadCostPerMillion: 7.5,
|
|
645
658
|
},
|
|
646
659
|
};
|
|
647
|
-
/**
|
|
648
|
-
|
|
660
|
+
/**
|
|
661
|
+
* Static-table keys sorted longest-first so longest-prefix wins. Computed on
|
|
662
|
+
* first lookup rather than at module load: a top-level call would keep the
|
|
663
|
+
* whole table alive in tree-shaken bundles that never price anything.
|
|
664
|
+
*/
|
|
665
|
+
let staticSortedKeys = null;
|
|
666
|
+
function getStaticSortedKeys() {
|
|
667
|
+
if (!staticSortedKeys) {
|
|
668
|
+
staticSortedKeys = Object.keys(PRICING_TABLE).sort((a, b) => b.length - a.length);
|
|
669
|
+
}
|
|
670
|
+
return staticSortedKeys;
|
|
671
|
+
}
|
|
649
672
|
// ── Override Table (populated at runtime by pricingCatalog) ──
|
|
650
673
|
let overrideTable = {};
|
|
651
674
|
let overrideSortedKeys = [];
|
|
675
|
+
let overrideBasis = null;
|
|
652
676
|
let importedTable = {};
|
|
653
677
|
/**
|
|
654
678
|
* Internal: replace the runtime override map. Called by `pricingCatalog.ts`
|
|
655
679
|
* after a successful LiteLLM hydration. Not part of the stable public API,
|
|
656
680
|
* but exported so the Node-only catalog module can wire itself in without
|
|
657
681
|
* creating a circular import.
|
|
682
|
+
*
|
|
683
|
+
* `basis` records how the map was obtained for `getPricingCatalogInfo()`;
|
|
684
|
+
* omitted, a non-empty map reports as `litellm` with no `fetchedAt`. Every
|
|
685
|
+
* call replaces the previous basis.
|
|
658
686
|
*/
|
|
659
|
-
function _setPricingOverrides(overrides) {
|
|
687
|
+
function _setPricingOverrides(overrides, basis) {
|
|
660
688
|
overrideTable = Object.fromEntries(Object.entries(overrides).map(([key, value]) => [key.trim().toLowerCase(), { ...value }]));
|
|
661
689
|
overrideSortedKeys = Object.keys(overrideTable).sort((a, b) => b.length - a.length);
|
|
690
|
+
overrideBasis = basis ? { ...basis } : null;
|
|
691
|
+
invalidatePricingDigest();
|
|
662
692
|
}
|
|
663
693
|
/** Internal: snapshot current overrides (test + diagnostics). */
|
|
664
694
|
function _getPricingOverrides() {
|
|
@@ -668,6 +698,8 @@ function _getPricingOverrides() {
|
|
|
668
698
|
function _clearPricingOverrides() {
|
|
669
699
|
overrideTable = {};
|
|
670
700
|
overrideSortedKeys = [];
|
|
701
|
+
overrideBasis = null;
|
|
702
|
+
invalidatePricingDigest();
|
|
671
703
|
}
|
|
672
704
|
/**
|
|
673
705
|
* Internal: hydrate exact pricing resolutions exported by another realm.
|
|
@@ -676,10 +708,210 @@ function _clearPricingOverrides() {
|
|
|
676
708
|
*/
|
|
677
709
|
function _setImportedResolvedPricing(values) {
|
|
678
710
|
importedTable = { ...importedTable, ...values };
|
|
711
|
+
invalidatePricingDigest();
|
|
679
712
|
}
|
|
680
713
|
/** Internal test hook. */
|
|
681
714
|
function _clearImportedResolvedPricing() {
|
|
682
715
|
importedTable = {};
|
|
716
|
+
invalidatePricingDigest();
|
|
717
|
+
}
|
|
718
|
+
// A Map (created on first registration) so keys such as `__proto__` stay data.
|
|
719
|
+
let registeredTable = null;
|
|
720
|
+
function isValidModelPricing(value) {
|
|
721
|
+
if (!value || typeof value !== 'object')
|
|
722
|
+
return false;
|
|
723
|
+
const pricing = value;
|
|
724
|
+
return [
|
|
725
|
+
pricing.inputCostPerMillion,
|
|
726
|
+
pricing.outputCostPerMillion,
|
|
727
|
+
pricing.cacheWriteCostPerMillion,
|
|
728
|
+
pricing.cacheReadCostPerMillion,
|
|
729
|
+
].every((item) => typeof item === 'number' && Number.isFinite(item) && item >= 0);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Register consumer pricing for model ids, e.g. negotiated or private-deployment
|
|
733
|
+
* rates. This layer is separate from the runtime catalog and imported layers:
|
|
734
|
+
* `_setPricingOverrides`, `hydratePricingCatalog`, and
|
|
735
|
+
* `importResolvedModelCatalog` never reset it; only
|
|
736
|
+
* `clearRegisteredModelPricing` does.
|
|
737
|
+
*
|
|
738
|
+
* Keys are trimmed, lowercased, and stripped of `[1m]`, like the other tables.
|
|
739
|
+
* Matching is exact, never by prefix, against the looked-up id, its registered
|
|
740
|
+
* alias target, and its `canonicalModelId()` form — so a `claude-opus-5-5`
|
|
741
|
+
* entry also answers `claude-opus-5-5-20261001` and
|
|
742
|
+
* `us.anthropic.claude-opus-5-5-20261001-v1:0`.
|
|
743
|
+
*
|
|
744
|
+
* Successive calls merge; re-registering a key replaces its price and mode.
|
|
745
|
+
* Entries whose four rates are not all finite, nonnegative numbers are
|
|
746
|
+
* skipped. Results carry `provenance.source: 'registered'`.
|
|
747
|
+
*
|
|
748
|
+
* Registered entries are realm-local configuration and are kept out of
|
|
749
|
+
* cross-realm snapshots: they are not listed by the catalog id enumeration,
|
|
750
|
+
* and `resolveModelPricing(id, { includeRegistered: false })` is the
|
|
751
|
+
* resolution a snapshot should carry. An exported price would otherwise be
|
|
752
|
+
* pinned as an exact import in the other realm and outlive
|
|
753
|
+
* `clearRegisteredModelPricing` there. Register in each realm instead.
|
|
754
|
+
*/
|
|
755
|
+
function registerModelPricing(entries, options) {
|
|
756
|
+
const onlyIfMissing = options?.onlyIfMissing === true;
|
|
757
|
+
let changed = false;
|
|
758
|
+
for (const [key, value] of Object.entries(entries ?? {})) {
|
|
759
|
+
const id = key
|
|
760
|
+
.replace(/\[1m\]/gi, '')
|
|
761
|
+
.trim()
|
|
762
|
+
.toLowerCase();
|
|
763
|
+
if (!id || !isValidModelPricing(value))
|
|
764
|
+
continue;
|
|
765
|
+
registeredTable ??= new Map();
|
|
766
|
+
registeredTable.set(id, {
|
|
767
|
+
pricing: {
|
|
768
|
+
inputCostPerMillion: value.inputCostPerMillion,
|
|
769
|
+
outputCostPerMillion: value.outputCostPerMillion,
|
|
770
|
+
cacheWriteCostPerMillion: value.cacheWriteCostPerMillion,
|
|
771
|
+
cacheReadCostPerMillion: value.cacheReadCostPerMillion,
|
|
772
|
+
},
|
|
773
|
+
onlyIfMissing,
|
|
774
|
+
});
|
|
775
|
+
changed = true;
|
|
776
|
+
}
|
|
777
|
+
if (changed)
|
|
778
|
+
invalidatePricingDigest();
|
|
779
|
+
}
|
|
780
|
+
/** Remove every entry added by `registerModelPricing`. */
|
|
781
|
+
function clearRegisteredModelPricing() {
|
|
782
|
+
if (registeredTable && registeredTable.size > 0)
|
|
783
|
+
invalidatePricingDigest();
|
|
784
|
+
registeredTable = null;
|
|
785
|
+
}
|
|
786
|
+
function findRegisteredPricing(rawId, aliasId) {
|
|
787
|
+
if (!registeredTable || registeredTable.size === 0)
|
|
788
|
+
return null;
|
|
789
|
+
const canonical = canonicalModelId(rawId);
|
|
790
|
+
for (const id of [rawId, aliasId, canonical, (0, modelAliases_1.resolveModelAlias)(canonical)]) {
|
|
791
|
+
const entry = registeredTable.get(id);
|
|
792
|
+
if (entry)
|
|
793
|
+
return { id, entry };
|
|
794
|
+
}
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
// ── Pricing Basis ──
|
|
798
|
+
let pricingDigestMemo = null;
|
|
799
|
+
function invalidatePricingDigest() {
|
|
800
|
+
pricingDigestMemo = null;
|
|
801
|
+
}
|
|
802
|
+
/** JSON with object keys sorted at every level; `undefined` members are dropped. */
|
|
803
|
+
function canonicalJson(value) {
|
|
804
|
+
if (value === null || typeof value !== 'object') {
|
|
805
|
+
return JSON.stringify(value) ?? 'null';
|
|
806
|
+
}
|
|
807
|
+
if (Array.isArray(value))
|
|
808
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
809
|
+
const record = value;
|
|
810
|
+
const keys = Object.keys(record)
|
|
811
|
+
.filter((key) => record[key] !== undefined)
|
|
812
|
+
.sort();
|
|
813
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* 64-bit FNV-1a over the UTF-8 bytes of `text`, as 16 lowercase hex digits.
|
|
817
|
+
* Uses four 16-bit limbs so it runs on plain numbers in every JS runtime.
|
|
818
|
+
*/
|
|
819
|
+
function fnv1a64Hex(text) {
|
|
820
|
+
// Offset basis 0xcbf29ce484222325, least significant limb first.
|
|
821
|
+
let h0 = 0x2325;
|
|
822
|
+
let h1 = 0x8422;
|
|
823
|
+
let h2 = 0x9ce4;
|
|
824
|
+
let h3 = 0xcbf2;
|
|
825
|
+
const mix = (byte) => {
|
|
826
|
+
h0 ^= byte;
|
|
827
|
+
// Multiply by the FNV prime 0x100000001b3 = 2^40 + 0x1b3, mod 2^64.
|
|
828
|
+
const t0 = h0 * 0x1b3;
|
|
829
|
+
const t1 = h1 * 0x1b3 + (t0 >>> 16);
|
|
830
|
+
const t2 = h2 * 0x1b3 + (h0 << 8) + (t1 >>> 16);
|
|
831
|
+
const t3 = h3 * 0x1b3 + (h1 << 8) + (t2 >>> 16);
|
|
832
|
+
h0 = t0 & 0xffff;
|
|
833
|
+
h1 = t1 & 0xffff;
|
|
834
|
+
h2 = t2 & 0xffff;
|
|
835
|
+
h3 = t3 & 0xffff;
|
|
836
|
+
};
|
|
837
|
+
for (let i = 0; i < text.length; i++) {
|
|
838
|
+
let code = text.charCodeAt(i);
|
|
839
|
+
if (code >= 0xd800 && code <= 0xdfff) {
|
|
840
|
+
const low = code <= 0xdbff && i + 1 < text.length ? text.charCodeAt(i + 1) : 0;
|
|
841
|
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
842
|
+
code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00);
|
|
843
|
+
i++;
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
// A lone surrogate encodes as U+FFFD, as TextEncoder does.
|
|
847
|
+
code = 0xfffd;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (code < 0x80) {
|
|
851
|
+
mix(code);
|
|
852
|
+
}
|
|
853
|
+
else if (code < 0x800) {
|
|
854
|
+
mix(0xc0 | (code >> 6));
|
|
855
|
+
mix(0x80 | (code & 0x3f));
|
|
856
|
+
}
|
|
857
|
+
else if (code < 0x10000) {
|
|
858
|
+
mix(0xe0 | (code >> 12));
|
|
859
|
+
mix(0x80 | ((code >> 6) & 0x3f));
|
|
860
|
+
mix(0x80 | (code & 0x3f));
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
mix(0xf0 | (code >> 18));
|
|
864
|
+
mix(0x80 | ((code >> 12) & 0x3f));
|
|
865
|
+
mix(0x80 | ((code >> 6) & 0x3f));
|
|
866
|
+
mix(0x80 | (code & 0x3f));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return [h3, h2, h1, h0].map((limb) => limb.toString(16).padStart(4, '0')).join('');
|
|
870
|
+
}
|
|
871
|
+
function getPricingDigest() {
|
|
872
|
+
if (pricingDigestMemo === null) {
|
|
873
|
+
const imported = {};
|
|
874
|
+
for (const [id, entry] of Object.entries(importedTable)) {
|
|
875
|
+
imported[id] = { pricing: entry.pricing, provenance: entry.provenance };
|
|
876
|
+
}
|
|
877
|
+
const registered = {};
|
|
878
|
+
for (const [id, entry] of registeredTable ?? []) {
|
|
879
|
+
registered[id] = { pricing: entry.pricing, onlyIfMissing: entry.onlyIfMissing };
|
|
880
|
+
}
|
|
881
|
+
const payload = canonicalJson({
|
|
882
|
+
v: 1,
|
|
883
|
+
static: PRICING_TABLE,
|
|
884
|
+
catalog: overrideTable,
|
|
885
|
+
imported,
|
|
886
|
+
registered,
|
|
887
|
+
});
|
|
888
|
+
pricingDigestMemo = `fnv1a64:${fnv1a64Hex(payload)}`;
|
|
889
|
+
}
|
|
890
|
+
return pricingDigestMemo;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Describe the pricing basis behind current cost estimates: which layer the
|
|
894
|
+
* prices come from, when the catalog was fetched, and a digest that changes
|
|
895
|
+
* whenever any active price changes. Pure and cheap after the first call (the
|
|
896
|
+
* digest is memoized until a table changes).
|
|
897
|
+
*/
|
|
898
|
+
function getPricingCatalogInfo() {
|
|
899
|
+
const catalogEntries = overrideSortedKeys.length;
|
|
900
|
+
let source = 'static';
|
|
901
|
+
if (catalogEntries > 0) {
|
|
902
|
+
source = overrideBasis?.source ?? 'litellm';
|
|
903
|
+
}
|
|
904
|
+
else if (Object.keys(importedTable).length > 0) {
|
|
905
|
+
source = 'imported';
|
|
906
|
+
}
|
|
907
|
+
const fetchedAt = catalogEntries > 0 ? overrideBasis?.fetchedAt : undefined;
|
|
908
|
+
return {
|
|
909
|
+
source,
|
|
910
|
+
...(fetchedAt ? { fetchedAt } : {}),
|
|
911
|
+
digest: getPricingDigest(),
|
|
912
|
+
catalogEntries,
|
|
913
|
+
registeredEntries: registeredTable?.size ?? 0,
|
|
914
|
+
};
|
|
683
915
|
}
|
|
684
916
|
/** Internal: list every ID known to the pricing resolver. */
|
|
685
917
|
function _getModelPricingIds() {
|
|
@@ -689,6 +921,86 @@ function _getModelPricingIds() {
|
|
|
689
921
|
...Object.keys(importedTable),
|
|
690
922
|
]));
|
|
691
923
|
}
|
|
924
|
+
// ── Model ID Canonicalization ──
|
|
925
|
+
// Bedrock: optional cross-region prefix (`us.`, `eu.`, `apac.`, `global.`, …)
|
|
926
|
+
// then the vendor segment, e.g. `us.anthropic.claude-opus-5-5-20261001-v1:0`.
|
|
927
|
+
const BEDROCK_ANTHROPIC_RE = /^(?:[a-z]{2,6}(?:-[a-z]{2,4})?\.)?anthropic\./;
|
|
928
|
+
// Bedrock revision tag (`-v1:0`, `-v1`), only after a version or date digit so
|
|
929
|
+
// legacy names such as `claude-v2:1` and `claude-instant-v1` keep theirs.
|
|
930
|
+
const BEDROCK_REVISION_RE = /([0-9])-v[0-9]+(?::[0-9]+)?$/;
|
|
931
|
+
const VERTEX_VERSION_RE = /@(?:[0-9]{8}|latest)$/;
|
|
932
|
+
const LATEST_SUFFIX_RE = /-latest$/;
|
|
933
|
+
const DASHED_DATE_SUFFIX_RE = /-[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
934
|
+
const COMPACT_DATE_SUFFIX_RE = /-[0-9]{8}$/;
|
|
935
|
+
/**
|
|
936
|
+
* Reduce a model id to the bare model name it denotes, for grouping and
|
|
937
|
+
* matching across hosts and snapshots.
|
|
938
|
+
*
|
|
939
|
+
* Trims and lowercases, then removes: `[1m]`; every leading `segment/`
|
|
940
|
+
* provider or region prefix (`azure/us/gpt-6.1-terra` → `gpt-6.1-terra`);
|
|
941
|
+
* Bedrock forms (`us.anthropic.claude-opus-5-5-20261001-v1:0` →
|
|
942
|
+
* `claude-opus-5-5`, also `eu.`/`apac.`/`global.` and bare `anthropic.`);
|
|
943
|
+
* Vertex `@YYYYMMDD`; trailing `-YYYYMMDD` and `-YYYY-MM-DD` dates; `-latest`.
|
|
944
|
+
* Meaningful suffixes (`-mini`, `-codex`, `-sol`, `-terra`, `-high`, …) stay.
|
|
945
|
+
*
|
|
946
|
+
* Pure and syntactic: registered aliases are not applied (see
|
|
947
|
+
* `resolveModelAlias`). Pricing lookups do not use this to reach other
|
|
948
|
+
* models' prices — dated snapshots and deployments can bill differently — only
|
|
949
|
+
* to match `registerModelPricing` entries.
|
|
950
|
+
*/
|
|
951
|
+
function canonicalModelId(id) {
|
|
952
|
+
let value = String(id ?? '')
|
|
953
|
+
.trim()
|
|
954
|
+
.toLowerCase()
|
|
955
|
+
.replace(/\[1m\]/g, '')
|
|
956
|
+
.trim();
|
|
957
|
+
const slash = value.lastIndexOf('/');
|
|
958
|
+
if (slash >= 0 && slash < value.length - 1)
|
|
959
|
+
value = value.slice(slash + 1);
|
|
960
|
+
const bedrock = BEDROCK_ANTHROPIC_RE.exec(value);
|
|
961
|
+
if (bedrock && value.length > bedrock[0].length) {
|
|
962
|
+
value = value.slice(bedrock[0].length).replace(BEDROCK_REVISION_RE, '$1');
|
|
963
|
+
}
|
|
964
|
+
// Suffixes can stack (`-20250101-latest`), so strip until nothing changes.
|
|
965
|
+
for (;;) {
|
|
966
|
+
const next = value
|
|
967
|
+
.replace(VERTEX_VERSION_RE, '')
|
|
968
|
+
.replace(LATEST_SUFFIX_RE, '')
|
|
969
|
+
.replace(DASHED_DATE_SUFFIX_RE, '')
|
|
970
|
+
.replace(COMPACT_DATE_SUFFIX_RE, '');
|
|
971
|
+
if (next === value || !next)
|
|
972
|
+
break;
|
|
973
|
+
value = next;
|
|
974
|
+
}
|
|
975
|
+
return value;
|
|
976
|
+
}
|
|
977
|
+
// Names an agent CLI re-points at a different concrete model over time.
|
|
978
|
+
// An object literal rather than a Set keeps it free for tree-shaking.
|
|
979
|
+
const MOVING_MODEL_ALIASES = {
|
|
980
|
+
default: true,
|
|
981
|
+
auto: true,
|
|
982
|
+
latest: true,
|
|
983
|
+
sonnet: true,
|
|
984
|
+
opus: true,
|
|
985
|
+
haiku: true,
|
|
986
|
+
fable: true,
|
|
987
|
+
opusplan: true,
|
|
988
|
+
};
|
|
989
|
+
/**
|
|
990
|
+
* True for model names an agent CLI resolves to a different concrete model
|
|
991
|
+
* over time (`default`, `auto`, `latest`, `sonnet`, `opus`, `haiku`, `fable`,
|
|
992
|
+
* and Claude Code's `opusplan`), so a recorded value names a moving target
|
|
993
|
+
* rather than a model. Case-insensitive, trimmed, and `[1m]`-tolerant
|
|
994
|
+
* (`sonnet[1m]`). Dated or versioned ids are never moving aliases.
|
|
995
|
+
*/
|
|
996
|
+
function isMovingModelAlias(id) {
|
|
997
|
+
const value = String(id ?? '')
|
|
998
|
+
.trim()
|
|
999
|
+
.toLowerCase()
|
|
1000
|
+
.replace(/\[1m\]/g, '')
|
|
1001
|
+
.trim();
|
|
1002
|
+
return Object.prototype.hasOwnProperty.call(MOVING_MODEL_ALIASES, value);
|
|
1003
|
+
}
|
|
692
1004
|
// ── Model ID Parsing ──
|
|
693
1005
|
// Anthropic writes the minor version with a hyphen in modern IDs
|
|
694
1006
|
// (`claude-sonnet-4-6-20260321`) and a dot in older ones
|
|
@@ -700,6 +1012,14 @@ const LEGACY_CLAUDE_RE = /^claude-([0-9]+(?:[-.][0-9]+)?)-(haiku|sonnet|opus)(?:
|
|
|
700
1012
|
const GPT_RE = /^gpt-([0-9][0-9.A-Za-z-]*)/i;
|
|
701
1013
|
const O_SERIES_RE = /^o([0-9]+)(-mini|-pro)?/i;
|
|
702
1014
|
const GEMINI_RE = /^gemini-([0-9][0-9.A-Za-z-]*)/i;
|
|
1015
|
+
// GPT tier names, read from the parsed version (`6.1-sol`, `6-luna-2026-01-01`).
|
|
1016
|
+
const GPT_TIER_VARIANT_RE = /^[0-9]+(?:\.[0-9]+)*-(astra|sol|terra|luna)(?=-|$)/;
|
|
1017
|
+
const GPT_TIER_VARIANT_RANK = {
|
|
1018
|
+
astra: 0,
|
|
1019
|
+
sol: 1,
|
|
1020
|
+
terra: 2,
|
|
1021
|
+
luna: 3,
|
|
1022
|
+
};
|
|
703
1023
|
/**
|
|
704
1024
|
* Parses a model ID into {provider, family, version}.
|
|
705
1025
|
*
|
|
@@ -731,7 +1051,13 @@ function parseModelId(modelId) {
|
|
|
731
1051
|
}
|
|
732
1052
|
const gpt = normalized.match(GPT_RE);
|
|
733
1053
|
if (gpt) {
|
|
734
|
-
|
|
1054
|
+
const version = gpt[1];
|
|
1055
|
+
const variant = GPT_TIER_VARIANT_RE.exec(version)?.[1];
|
|
1056
|
+
// The key is only present when set, so untiered results stay deep-equal
|
|
1057
|
+
// to what earlier releases returned.
|
|
1058
|
+
return variant
|
|
1059
|
+
? { provider: 'openai', family: 'gpt', version, variant }
|
|
1060
|
+
: { provider: 'openai', family: 'gpt', version };
|
|
735
1061
|
}
|
|
736
1062
|
const oSeries = normalized.match(O_SERIES_RE);
|
|
737
1063
|
if (oSeries) {
|
|
@@ -753,24 +1079,45 @@ function findLongestPrefix(keys, modelId) {
|
|
|
753
1079
|
}
|
|
754
1080
|
return null;
|
|
755
1081
|
}
|
|
756
|
-
/**
|
|
757
|
-
|
|
1082
|
+
/**
|
|
1083
|
+
* Resolve pricing and disclose which layer answered and whether the result was
|
|
1084
|
+
* inherited by prefix.
|
|
1085
|
+
*
|
|
1086
|
+
* Order: registered entries (default mode); exact imported, catalog, and
|
|
1087
|
+
* static matches; registered `onlyIfMissing` entries; catalog then static
|
|
1088
|
+
* prefix matches; unpriced.
|
|
1089
|
+
*/
|
|
1090
|
+
function resolveModelPricing(modelId, options) {
|
|
758
1091
|
const stripped = modelId.replace(/\[1m\]/gi, '');
|
|
759
1092
|
const rawId = stripped.trim().toLowerCase();
|
|
760
|
-
const
|
|
1093
|
+
const aliasId = (0, modelAliases_1.resolveModelAlias)(stripped);
|
|
761
1094
|
// The raw id is tried before its alias target at every step: pricing
|
|
762
1095
|
// recorded for the alias id itself is more specific than the canonical
|
|
763
1096
|
// fallback, so registering an alias never orphans it.
|
|
764
|
-
const candidateIds = rawId ===
|
|
765
|
-
const
|
|
1097
|
+
const candidateIds = rawId === aliasId ? [aliasId] : [rawId, aliasId];
|
|
1098
|
+
const registered = options?.includeRegistered === false ? null : findRegisteredPricing(rawId, aliasId);
|
|
1099
|
+
const registeredResult = (match) => ({
|
|
1100
|
+
modelId,
|
|
1101
|
+
canonicalModelId: aliasId,
|
|
1102
|
+
pricing: { ...match.entry.pricing },
|
|
1103
|
+
provenance: {
|
|
1104
|
+
source: 'registered',
|
|
1105
|
+
match: 'exact',
|
|
1106
|
+
matchedModelId: match.id,
|
|
1107
|
+
inheritedByPrefix: false,
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1110
|
+
if (registered && !registered.entry.onlyIfMissing)
|
|
1111
|
+
return registeredResult(registered);
|
|
1112
|
+
const imported = (0, ownKey_1.ownValue)(importedTable, rawId) ?? (0, ownKey_1.ownValue)(importedTable, aliasId);
|
|
766
1113
|
if (imported)
|
|
767
|
-
return { ...imported, modelId, canonicalModelId };
|
|
1114
|
+
return { ...imported, modelId, canonicalModelId: aliasId };
|
|
768
1115
|
for (const id of candidateIds) {
|
|
769
|
-
const exactOverride = overrideTable
|
|
1116
|
+
const exactOverride = (0, ownKey_1.ownValue)(overrideTable, id);
|
|
770
1117
|
if (exactOverride) {
|
|
771
1118
|
return {
|
|
772
1119
|
modelId,
|
|
773
|
-
canonicalModelId,
|
|
1120
|
+
canonicalModelId: aliasId,
|
|
774
1121
|
pricing: exactOverride,
|
|
775
1122
|
provenance: {
|
|
776
1123
|
source: 'catalog',
|
|
@@ -782,11 +1129,11 @@ function resolveModelPricing(modelId) {
|
|
|
782
1129
|
}
|
|
783
1130
|
}
|
|
784
1131
|
for (const id of candidateIds) {
|
|
785
|
-
const exactStatic = PRICING_TABLE
|
|
1132
|
+
const exactStatic = (0, ownKey_1.ownValue)(PRICING_TABLE, id);
|
|
786
1133
|
if (exactStatic) {
|
|
787
1134
|
return {
|
|
788
1135
|
modelId,
|
|
789
|
-
canonicalModelId,
|
|
1136
|
+
canonicalModelId: aliasId,
|
|
790
1137
|
pricing: exactStatic,
|
|
791
1138
|
provenance: {
|
|
792
1139
|
source: 'static',
|
|
@@ -797,12 +1144,15 @@ function resolveModelPricing(modelId) {
|
|
|
797
1144
|
};
|
|
798
1145
|
}
|
|
799
1146
|
}
|
|
1147
|
+
// A gap-filling registration answers once no layer has an exact price.
|
|
1148
|
+
if (registered)
|
|
1149
|
+
return registeredResult(registered);
|
|
800
1150
|
for (const id of candidateIds) {
|
|
801
1151
|
const overridePrefix = findLongestPrefix(overrideSortedKeys, id);
|
|
802
1152
|
if (overridePrefix) {
|
|
803
1153
|
return {
|
|
804
1154
|
modelId,
|
|
805
|
-
canonicalModelId,
|
|
1155
|
+
canonicalModelId: aliasId,
|
|
806
1156
|
pricing: overrideTable[overridePrefix],
|
|
807
1157
|
provenance: {
|
|
808
1158
|
source: 'catalog',
|
|
@@ -814,11 +1164,11 @@ function resolveModelPricing(modelId) {
|
|
|
814
1164
|
}
|
|
815
1165
|
}
|
|
816
1166
|
for (const id of candidateIds) {
|
|
817
|
-
const staticPrefix = findLongestPrefix(
|
|
1167
|
+
const staticPrefix = findLongestPrefix(getStaticSortedKeys(), id);
|
|
818
1168
|
if (staticPrefix) {
|
|
819
1169
|
return {
|
|
820
1170
|
modelId,
|
|
821
|
-
canonicalModelId,
|
|
1171
|
+
canonicalModelId: aliasId,
|
|
822
1172
|
pricing: PRICING_TABLE[staticPrefix],
|
|
823
1173
|
provenance: {
|
|
824
1174
|
source: 'static',
|
|
@@ -831,7 +1181,7 @@ function resolveModelPricing(modelId) {
|
|
|
831
1181
|
}
|
|
832
1182
|
return {
|
|
833
1183
|
modelId,
|
|
834
|
-
canonicalModelId,
|
|
1184
|
+
canonicalModelId: aliasId,
|
|
835
1185
|
pricing: null,
|
|
836
1186
|
provenance: { source: 'none', match: 'none', inheritedByPrefix: false },
|
|
837
1187
|
};
|
|
@@ -839,9 +1189,11 @@ function resolveModelPricing(modelId) {
|
|
|
839
1189
|
/**
|
|
840
1190
|
* Gets pricing for a model ID.
|
|
841
1191
|
*
|
|
842
|
-
* Lookup order:
|
|
843
|
-
* 1.
|
|
844
|
-
* 2.
|
|
1192
|
+
* Lookup order (see `resolveModelPricing` for provenance):
|
|
1193
|
+
* 1. Registered entries (`registerModelPricing`), exact matches only.
|
|
1194
|
+
* 2. Imported resolutions, then the runtime override map (from LiteLLM
|
|
1195
|
+
* catalog hydration), then the static PRICING_TABLE — exact before prefix,
|
|
1196
|
+
* with gap-filling registered entries between the two.
|
|
845
1197
|
* 3. `null` — unknown model. Callers MUST handle this.
|
|
846
1198
|
*
|
|
847
1199
|
* The ID is first looked up verbatim (minus the "[1m]" suffix) — override
|
|
@@ -933,22 +1285,47 @@ function mergeCostSources(a, b) {
|
|
|
933
1285
|
};
|
|
934
1286
|
return rank[a] >= rank[b] ? a : b;
|
|
935
1287
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1288
|
+
/**
|
|
1289
|
+
* Map the provenance `calculateNormalizedUsageCost` reports to the display
|
|
1290
|
+
* cost source: provider-reported costs are `reported`, costs computed from
|
|
1291
|
+
* explicit or catalog pricing are `estimated`, and unpriced stays `unpriced`.
|
|
1292
|
+
*/
|
|
1293
|
+
function costSourceFromPricingProvenance(provenance) {
|
|
1294
|
+
switch (provenance) {
|
|
1295
|
+
case 'provider-reported':
|
|
1296
|
+
return 'reported';
|
|
1297
|
+
case 'explicit-pricing':
|
|
1298
|
+
case 'model-catalog':
|
|
1299
|
+
return 'estimated';
|
|
1300
|
+
default:
|
|
1301
|
+
return 'unpriced';
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Merge the cost sources of a rollup's components, least certain winning (as
|
|
1306
|
+
* `mergeCostSources`). Components without a `source` have not been seen yet
|
|
1307
|
+
* and are ignored, and an `unpriced` component that contributed no tokens
|
|
1308
|
+
* does not taint the total. Returns `undefined` when nothing counted.
|
|
1309
|
+
*/
|
|
1310
|
+
function mergeCostComponents(components) {
|
|
1311
|
+
let merged;
|
|
1312
|
+
for (const component of components) {
|
|
1313
|
+
const source = component?.source;
|
|
1314
|
+
if (!source)
|
|
1315
|
+
continue;
|
|
1316
|
+
if (source === 'unpriced' && !(component.tokens > 0))
|
|
1317
|
+
continue;
|
|
1318
|
+
merged = merged ? mergeCostSources(merged, source) : source;
|
|
1319
|
+
}
|
|
1320
|
+
return merged;
|
|
945
1321
|
}
|
|
1322
|
+
// ── Display ──
|
|
946
1323
|
/**
|
|
947
1324
|
* Short display label for model IDs. Keeps historical Claude family labels
|
|
948
1325
|
* compact while normalizing common OpenAI labels.
|
|
949
1326
|
*/
|
|
950
1327
|
function shortModelName(modelId) {
|
|
951
|
-
const normalized =
|
|
1328
|
+
const normalized = canonicalModelId(modelId);
|
|
952
1329
|
const parsed = parseModelId(normalized);
|
|
953
1330
|
if (normalized.includes('codex'))
|
|
954
1331
|
return 'Codex';
|
|
@@ -987,7 +1364,7 @@ function versionRank(version) {
|
|
|
987
1364
|
*/
|
|
988
1365
|
function getModelDisplayInfo(modelId) {
|
|
989
1366
|
const parsed = parseModelId(modelId);
|
|
990
|
-
const normalized =
|
|
1367
|
+
const normalized = canonicalModelId(modelId);
|
|
991
1368
|
let rank = 1_000;
|
|
992
1369
|
if (parsed?.provider === 'anthropic') {
|
|
993
1370
|
rank = (CLAUDE_FAMILY_RANK[parsed.family] ?? 9) * 100 + versionRank(parsed.version);
|
|
@@ -1013,29 +1390,37 @@ function getModelDisplayInfo(modelId) {
|
|
|
1013
1390
|
rank,
|
|
1014
1391
|
};
|
|
1015
1392
|
}
|
|
1016
|
-
/**
|
|
1393
|
+
/**
|
|
1394
|
+
* Order two GPT tier variants of the same family and numeric version
|
|
1395
|
+
* (astra > sol > terra > luna); 0 for every other pair.
|
|
1396
|
+
*/
|
|
1397
|
+
function compareTierVariants(a, b) {
|
|
1398
|
+
const left = parseModelId(a);
|
|
1399
|
+
const right = parseModelId(b);
|
|
1400
|
+
if (!left?.variant || !right?.variant)
|
|
1401
|
+
return 0;
|
|
1402
|
+
if (left.provider !== right.provider || left.family !== right.family)
|
|
1403
|
+
return 0;
|
|
1404
|
+
if (versionRank(left.version) !== versionRank(right.version))
|
|
1405
|
+
return 0;
|
|
1406
|
+
return ((GPT_TIER_VARIANT_RANK[left.variant] ?? Number.MAX_SAFE_INTEGER) -
|
|
1407
|
+
(GPT_TIER_VARIANT_RANK[right.variant] ?? Number.MAX_SAFE_INTEGER));
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Compare two model IDs using shared provider/family ranking rules. Within a
|
|
1411
|
+
* family and numeric version, GPT tiers rank astra > sol > terra > luna.
|
|
1412
|
+
*/
|
|
1017
1413
|
function compareModelIds(a, b) {
|
|
1018
1414
|
const left = getModelDisplayInfo(a);
|
|
1019
1415
|
const right = getModelDisplayInfo(b);
|
|
1020
1416
|
if (left.rank !== right.rank)
|
|
1021
1417
|
return left.rank - right.rank;
|
|
1418
|
+
const variantOrder = compareTierVariants(a, b);
|
|
1419
|
+
if (variantOrder !== 0)
|
|
1420
|
+
return variantOrder;
|
|
1022
1421
|
return a.localeCompare(b);
|
|
1023
1422
|
}
|
|
1024
1423
|
/** Return a sorted copy of model IDs using shared provider/family ranking rules. */
|
|
1025
1424
|
function sortModelIds(modelIds) {
|
|
1026
1425
|
return [...modelIds].sort(compareModelIds);
|
|
1027
1426
|
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Formats a USD cost as a currency string.
|
|
1030
|
-
*
|
|
1031
|
-
* - `null` / `undefined` → `"—"` (honest "pricing unavailable").
|
|
1032
|
-
* - `< $0.01` → 4 decimals, so sub-cent costs are still visible.
|
|
1033
|
-
* - otherwise → 2 decimals.
|
|
1034
|
-
*/
|
|
1035
|
-
function formatCost(cost) {
|
|
1036
|
-
if (cost === null || cost === undefined)
|
|
1037
|
-
return '—';
|
|
1038
|
-
if (cost < 0.01)
|
|
1039
|
-
return `$${cost.toFixed(4)}`;
|
|
1040
|
-
return `$${cost.toFixed(2)}`;
|
|
1041
|
-
}
|