tokentracker-cli 1.4.6 → 1.4.9
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/README.md +9 -1
- package/README.zh-CN.md +1 -1
- package/bin/tracker.js +6 -0
- package/dashboard/dist/assets/{AchievementBadge-BmWRsZMx.js → AchievementBadge-_dRLgxnp.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CyIP2nck.js → AchievementsPage-DtvlZLlv.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-BeNyJ0LW.js → AchievementsSection-OgzKkbes.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DIFnFzkv.js → BadgeDetailModal-D4Rlm_um.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CKVeJHit.js → BotAnimated-errJ5IvB.js} +1 -1
- package/dashboard/dist/assets/{Card-lQmyKbh4.js → Card-esELVQcD.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-B_8y6dyX.js → CloudCheckoutPage-3sH4Dbx9.js} +1 -1
- package/dashboard/dist/assets/CloudPage-Bi_vgiqE.js +2 -0
- package/dashboard/dist/assets/{CommandPalette-BV9LqokI.js → CommandPalette-I0aze2EJ.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-WELxKEFc.js → CommunityStatsModal-CEn2g4J9.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-B8H5o8c_.js → ConfirmModal-D7SfFCaf.js} +1 -1
- package/dashboard/dist/assets/{Controls-9ynPx9rF.js → Controls-Dyb5Vmk_.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-CMHX8zDq.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-DEqWsV_8.js → DateRangePopover-CgmxsHVR.js} +1 -1
- package/dashboard/dist/assets/DevicePage-9R2MfK2Z.js +1 -0
- package/dashboard/dist/assets/{DialogClose-Dlg52bL6.js → DialogClose-DzR109OA.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CdNjUDLZ.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CduwXjqH.js → DialogTitle-BLH1MgOX.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-DFt7_F2f.js → ElectricLogo-DcpBcJIy.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DlyusE6n.js → FadeIn-CBAXe_zb.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-tUtJoFHh.js → HeaderGithubStar-BZjQ3uJo.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-v-zkCqsX.js → HoverTooltip-Bq4HuWOZ.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-C5Y1uVEr.js → IpCheckPage-Oiah1-YB.js} +1 -1
- package/dashboard/dist/assets/LandingPage-Cc1FfkWg.js +2 -0
- package/dashboard/dist/assets/{LeaderboardPage-BBZ_6tCw.js → LeaderboardPage-Bc2j_DQY.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-DakoXIQn.js → LeaderboardProBadge-w1olVHVp.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-CINpES_x.js → LeaderboardProfileModal-mgG7mdl8.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-B6ISf-1l.js → LeaderboardProfilePage-DqxyooT5.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-BIvK0GFj.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-BJSsmXF1.js → LocalOnlyNotice-DZLgI75v.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-Dggv2aGv.js → LoginCard-DVNU_QtS.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B8qDd2kY.js → LoginModal-yxi5pCwC.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-Dv66U8yJ.js → LoginPage-CW9pu4mf.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CNwrQdKM.js → PetPage-DwZsq4nC.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-CqmvpYOi.js → PopoverPopup-DYGmmoD8.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BF2Ch7wi.js → ProviderIcon-BKfj89GN.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B0R8VUU3.js → ResetPasswordPage-gQdgCp9s.js} +1 -1
- package/dashboard/dist/assets/{Select-idAiBnJG.js → Select-DvSR56Lj.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DPmmPY_D.js → SelectItemText-DXo30s3O.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-oR9cPK02.js → SelfHostPage-jA51Nckw.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-CdV9gfGF.js → ServiceStatusPage-DEkRRPx8.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-BkD34VlL.js → SessionsPage-J7hvGPCj.js} +3 -3
- package/dashboard/dist/assets/SettingsPage-ewH1Mzsm.js +5 -0
- package/dashboard/dist/assets/{SkillsPage-CzMk9s3x.js → SkillsPage-BaeBHhU7.js} +1 -1
- package/dashboard/dist/assets/TokenGalaxy-D52pHpzj.js +4426 -0
- package/dashboard/dist/assets/TooltipPopup-mLpESQcv.js +1 -0
- package/dashboard/dist/assets/TrendMonitor-ErowR5GE.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-Cc16WM0Y.js → WidgetsPage-Bf1uuKE7.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-pMwbM4R7.js → WrappedPage-C8qacvpR.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-CIpYjfAT.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-CnVnsbqV.css +1 -0
- package/dashboard/dist/assets/{agent-logos-Zw7LNkMO.js → agent-logos-tmdKvCcK.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-DycYjE-k.js → arrow-left-CRKi420c.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-uNxLHoJ0.js → arrow-right-C_o_iQbK.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CmdJ7gd7.js → arrow-up-right-N5YqKKnI.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-CJ33_t5O.js → calendar-days-BJh4FDj9.js} +1 -1
- package/dashboard/dist/assets/{check-Dbubk4Il.js → check-BfHGb92g.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BwvBVAOs.js → chevron-down-D1pO8Xgc.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-IG-IOpeP.js → cloud-checkout-Ds13TmYz.js} +1 -1
- package/dashboard/dist/assets/{composite-IIgJPPIw.js → composite-DDEsQKPD.js} +1 -1
- package/dashboard/dist/assets/copy-CJTosYLe.js +1 -0
- package/dashboard/dist/assets/{download-DxtH7w6a.js → download-Bkqx3c3y.js} +1 -1
- package/dashboard/dist/assets/{external-link-evKf3mnD.js → external-link-IYUo5LcK.js} +1 -1
- package/dashboard/dist/assets/{flame-D2qOE_W4.js → flame-BD_hiMfM.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CqDmm-sK.js → format-tokens-DxwbhPag.js} +1 -1
- package/dashboard/dist/assets/{gift-Ge11HK1x.js → gift-DLzFdL-1.js} +1 -1
- package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
- package/dashboard/dist/assets/{icons-OmfWvE5K.js → icons-CbrWeFPS.js} +1 -1
- package/dashboard/dist/assets/{index-BsYtyo4U.js → index-3xGpnZnP.js} +1 -1
- package/dashboard/dist/assets/{index-tbe1aERA.js → index-AVEWo-aY.js} +1 -1
- package/dashboard/dist/assets/{index-BYuEzP9p.js → index-DfzCE-FL.js} +1 -1
- package/dashboard/dist/assets/index-Dj1QnoPF.js +502 -0
- package/dashboard/dist/assets/{info-xhrso9aO.js → info-odlIlCnm.js} +1 -1
- package/dashboard/dist/assets/{link-2-BgNpTaO4.js → link-2-Bt28Ey9r.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DRooZ0e5.js → loader-circle-lGnIhIFS.js} +1 -1
- package/dashboard/dist/assets/local-usage-transfer-DSS0WaL-.js +1 -0
- package/dashboard/dist/assets/{modifiers.esm-D-QAEpOZ.js → modifiers.esm-CitRa_Z1.js} +1 -1
- package/dashboard/dist/assets/module-C2SOSUuR.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-Bwu7__2B.js → monitor-smartphone-1T0GFcOz.js} +1 -1
- package/dashboard/dist/assets/{play-CBPvTiWM.js → play-d6YYOmED.js} +1 -1
- package/dashboard/dist/assets/{plus-B4lFIyP8.js → plus-GrGV9Fkm.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CQlWusXH.js → provider-display-D1v3cgDm.js} +1 -1
- package/dashboard/dist/assets/{react-DDOtS4Bd.js → react-DYCAyQbo.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-BGfBVKoy.js → safePolygon-iPu060Qe.js} +1 -1
- package/dashboard/dist/assets/{search-DVyNq72J.js → search-AQhOTwzm.js} +1 -1
- package/dashboard/dist/assets/{server-EpUTEsOf.js → server-uYTbWPMd.js} +1 -1
- package/dashboard/dist/assets/{shield-check-DRg8cPKV.js → shield-check-DB1NVCDf.js} +1 -1
- package/dashboard/dist/assets/{skills-api-y42dEF96.js → skills-api-D7X10n0t.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-D2S1gXCK.js → sortable.esm-B13KhrCd.js} +1 -1
- package/dashboard/dist/assets/{store-DfGPS2IR.js → store-D6XHiq8o.js} +1 -1
- package/dashboard/dist/assets/{terminal-hXIrrL2b.js → terminal-t37F8qyF.js} +1 -1
- package/dashboard/dist/assets/{trash-cjaASRQl.js → trash-CDHRwGsy.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-B-nipjWt.js → use-community-stats--kaGg0EW.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DIEYrSzn.js → use-latest-request-guard-ChyODWU3.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BdHDwBBC.js → use-limits-display-prefs-katFAxOA.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-7w71_h44.js → use-native-settings-i3O4GTY1.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-CSMpK4ar.js → use-ordered-list-BtewEEX_.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DPacKl9n.js +1 -0
- package/dashboard/dist/assets/{use-transform-sjGGXCNn.js → use-transform-NBAyuphu.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Nml-wUB-.js → use-usage-limits-hsv-93TF.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DAKIY1nR.js → useCurrency-BdsxkAID.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-B7vVQ7th.js → usePositioner-BAN79miB.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-gtgw8k2Z.js → useTokenFormat-CGEO_mX1.js} +1 -1
- package/dashboard/dist/assets/{zap-DFWOiTJ1.js → zap-BDHWp-gL.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/privacy.html +8 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +3 -2
- package/src/cli.js +10 -0
- package/src/commands/sync.js +18 -3
- package/src/lib/codex-rollout-parser.js +29 -2
- package/src/lib/codex-service-tier.js +13 -0
- package/src/lib/commandcode-limits.js +27 -11
- package/src/lib/local-api.js +122 -11
- package/src/lib/local-usage-transfer.js +271 -0
- package/src/lib/opencode-go-limits.js +17 -6
- package/src/lib/pricing/curated-overrides.json +24 -0
- package/src/lib/pricing/index.js +50 -39
- package/src/lib/pricing/matcher.js +9 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-api-key-store.js +110 -0
- package/src/lib/rollout.js +27 -3
- package/src/lib/sentry.js +231 -0
- package/src/lib/session-analytics.js +15 -2
- package/src/lib/static-server.js +15 -2
- package/src/lib/usage-limits.js +54 -16
- package/dashboard/dist/assets/CloudPage-BOQjUGAt.js +0 -2
- package/dashboard/dist/assets/DashboardPage-B8z-U63p.js +0 -1
- package/dashboard/dist/assets/DevicePage-CUOlUySM.js +0 -1
- package/dashboard/dist/assets/DialogDescription-vuQFBlM_.js +0 -1
- package/dashboard/dist/assets/LandingPage-DzOJOAj7.js +0 -2
- package/dashboard/dist/assets/LimitsPage-UVX730CQ.js +0 -9
- package/dashboard/dist/assets/SettingsPage-DsthWCyM.js +0 -5
- package/dashboard/dist/assets/TokenGalaxy-DQ6o8YBp.js +0 -4426
- package/dashboard/dist/assets/TooltipPopup-CMQjA-HE.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-DrV1m8E7.js +0 -19
- package/dashboard/dist/assets/__vite-browser-external-B0m8a6Te.css +0 -1
- package/dashboard/dist/assets/__vite-browser-external-DCy_YLfe.js +0 -45
- package/dashboard/dist/assets/host-mode-8_TNh7yE.js +0 -1
- package/dashboard/dist/assets/module-DOCYJaxl.js +0 -3
- package/dashboard/dist/assets/use-reduced-motion-HUGf223R.js +0 -1
- package/dashboard/dist/assets/use-session-efficiency-pref-BagBVWW6.js +0 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
const { createReadStream } = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const crypto = require("node:crypto");
|
|
7
|
+
const { openLock, writeFileAtomic } = require("./fs");
|
|
8
|
+
const { isAccountLevelSource } = require("./source-metadata");
|
|
9
|
+
|
|
10
|
+
const SCHEMA = "tokentracker.local-usage.v1";
|
|
11
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
12
|
+
const MAX_STATE_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
const MAX_DEVICES = 32;
|
|
14
|
+
const TOKEN_FIELDS = ["input_tokens", "output_tokens", "cached_input_tokens", "cache_creation_input_tokens",
|
|
15
|
+
"reasoning_output_tokens", "total_tokens", "billable_total_tokens", "conversation_count",
|
|
16
|
+
...["long_context_", "priority_", "priority_long_context_"].flatMap((prefix) =>
|
|
17
|
+
["input_tokens", "output_tokens", "cached_input_tokens", "cache_creation_input_tokens", "reasoning_output_tokens"].map((field) => prefix + field))];
|
|
18
|
+
const ROW_FIELDS = new Set(["source", "model", "hour_start", "total_cost_usd", ...TOKEN_FIELDS]);
|
|
19
|
+
const validId = (id) => typeof id === "string" && /^[A-Za-z0-9_-]{8,128}$/.test(id);
|
|
20
|
+
const validTime = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
21
|
+
const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
22
|
+
function fail(code, status = 400) { throw Object.assign(new Error(code), { code, status }); }
|
|
23
|
+
function count(rows, field = "total_tokens") {
|
|
24
|
+
const result = rows.reduce((sum, row) => sum + (row[field] ?? row.total_tokens), 0);
|
|
25
|
+
if (!Number.isSafeInteger(result)) fail("invalid_snapshot");
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
function displayCount(rows) {
|
|
29
|
+
const total = count(rows);
|
|
30
|
+
return count(rows, "billable_total_tokens") || total;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cleanRow(row, strict = false) {
|
|
34
|
+
if (!row || typeof row !== "object" || Array.isArray(row) ||
|
|
35
|
+
!Number.isSafeInteger(row.total_tokens) || row.total_tokens < 0 ||
|
|
36
|
+
typeof row.source !== "string" || !/^[a-z0-9_-]{1,80}$/.test(row.source) ||
|
|
37
|
+
typeof row.model !== "string" || !row.model.trim() || row.model.length > 200 || /[\x00-\x1f]/.test(row.model) ||
|
|
38
|
+
!validTime(row.hour_start) || Date.parse(row.hour_start) % 1_800_000 !== 0 ||
|
|
39
|
+
(strict && Object.keys(row).some((key) => !ROW_FIELDS.has(key)))) fail("invalid_snapshot");
|
|
40
|
+
const result = { source: row.source, model: row.model, hour_start: row.hour_start };
|
|
41
|
+
for (const field of TOKEN_FIELDS) {
|
|
42
|
+
const value = row[field] ?? (field === "billable_total_tokens" ? row.total_tokens : 0);
|
|
43
|
+
if (!Number.isSafeInteger(value) || value < 0) fail("invalid_snapshot");
|
|
44
|
+
if (value || Object.hasOwn(row, field) || field === "total_tokens" || field === "billable_total_tokens") result[field] = value;
|
|
45
|
+
}
|
|
46
|
+
if (Object.hasOwn(row, "total_cost_usd")) {
|
|
47
|
+
if (!Number.isFinite(row.total_cost_usd) || row.total_cost_usd < 0) fail("invalid_snapshot");
|
|
48
|
+
result.total_cost_usd = row.total_cost_usd;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateSnapshot(value) {
|
|
54
|
+
if (!value || value.schema !== SCHEMA || value.scope !== "device-history" ||
|
|
55
|
+
Object.keys(value).some((key) => !["schema", "scope", "machine", "sequence", "exported_at", "rows"].includes(key)) ||
|
|
56
|
+
!validId(value.machine?.id) || typeof value.machine.name !== "string" || value.machine.name.length > 128 ||
|
|
57
|
+
/[\x00-\x1f]/.test(value.machine.name) || typeof value.machine.platform !== "string" || value.machine.platform.length > 30 ||
|
|
58
|
+
Object.keys(value.machine).some((key) => !["id", "name", "platform"].includes(key)) ||
|
|
59
|
+
!Number.isSafeInteger(value.sequence) || value.sequence < 1 || !validTime(value.exported_at) ||
|
|
60
|
+
!Array.isArray(value.rows) || value.rows.length > 100_000 || Buffer.byteLength(JSON.stringify(value)) > MAX_BYTES) fail("invalid_snapshot");
|
|
61
|
+
const rows = value.rows.map((row) => cleanRow(row, true));
|
|
62
|
+
const keys = new Set();
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const key = JSON.stringify([row.source, row.model, row.hour_start]);
|
|
65
|
+
if (isAccountLevelSource(row.source) || keys.has(key)) fail("invalid_snapshot");
|
|
66
|
+
keys.add(key);
|
|
67
|
+
}
|
|
68
|
+
count(rows);
|
|
69
|
+
// cleanRow may insert billable_total_tokens and drop omitted zeros, so the
|
|
70
|
+
// serialized size after normalization can exceed the pre-clean check above.
|
|
71
|
+
// Reject before commit: an accepted oversize snapshot would make later
|
|
72
|
+
// validateSnapshot reads fail and hide the import from Settings removal.
|
|
73
|
+
const normalized = { ...value, machine: { ...value.machine }, rows };
|
|
74
|
+
if (Buffer.byteLength(JSON.stringify(normalized)) > MAX_BYTES) fail("invalid_snapshot");
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Imported snapshots never enter the append-only queue or its cloud upload offset.
|
|
79
|
+
// Keep rows separate until aggregation so pricing annotations and device identity
|
|
80
|
+
// survive (e.g. reported costs and long-context/priority subsets).
|
|
81
|
+
function createLocalUsageTransfer({ queuePath, normalizeRow, identity }) {
|
|
82
|
+
const directory = path.dirname(queuePath);
|
|
83
|
+
const statePath = path.join(directory, "local-usage.json");
|
|
84
|
+
let cachedRaw;
|
|
85
|
+
let cachedState;
|
|
86
|
+
let cachedView;
|
|
87
|
+
const importedCache = new WeakMap();
|
|
88
|
+
async function state() {
|
|
89
|
+
try {
|
|
90
|
+
const chunks = [];
|
|
91
|
+
let size = 0;
|
|
92
|
+
// Open once and bound the actual read, including a possible growth or
|
|
93
|
+
// replacement since another process last observed the ledger's size.
|
|
94
|
+
for await (const chunk of createReadStream(statePath, { end: MAX_STATE_BYTES })) {
|
|
95
|
+
size += chunk.length;
|
|
96
|
+
if (size > MAX_STATE_BYTES) fail("invalid_state", 503);
|
|
97
|
+
chunks.push(chunk);
|
|
98
|
+
}
|
|
99
|
+
const raw = Buffer.concat(chunks, size).toString("utf8");
|
|
100
|
+
if (raw === cachedRaw) return cachedState;
|
|
101
|
+
const value = JSON.parse(raw);
|
|
102
|
+
if (value.version !== 1 || !Number.isSafeInteger(value.revision) || value.revision < 0 ||
|
|
103
|
+
!Number.isSafeInteger(value.sequence) || value.sequence < 0 || !value.imports ||
|
|
104
|
+
typeof value.imports !== "object" || Array.isArray(value.imports) || Object.keys(value.imports).length > MAX_DEVICES) fail("invalid_state", 503);
|
|
105
|
+
cachedRaw = raw;
|
|
106
|
+
cachedState = value;
|
|
107
|
+
return value;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error.code === "ENOENT") return { version: 1, revision: 0, sequence: 0, imports: {} };
|
|
110
|
+
fail("invalid_state", 503);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function ownRows() {
|
|
114
|
+
let raw;
|
|
115
|
+
try { raw = await fs.readFile(queuePath, "utf8"); }
|
|
116
|
+
catch (error) { if (error.code === "ENOENT") return []; throw error; }
|
|
117
|
+
const rows = new Map();
|
|
118
|
+
for (const line of raw.split("\n")) {
|
|
119
|
+
if (!line.trim()) continue;
|
|
120
|
+
let row;
|
|
121
|
+
try { row = JSON.parse(line); } catch { fail("invalid_queue", 409); }
|
|
122
|
+
if (!row || typeof row !== "object") fail("invalid_queue", 409);
|
|
123
|
+
if (Object.hasOwn(row, "_ttsync")) fail("legacy_imports", 409);
|
|
124
|
+
if (row.kind) continue;
|
|
125
|
+
const normalized = normalizeRow(row);
|
|
126
|
+
if (!Number.isFinite(Date.parse(normalized.hour_start))) fail("invalid_queue", 409);
|
|
127
|
+
// A field allowlist is essential: never export arbitrary parser metadata.
|
|
128
|
+
const clean = cleanRow({ ...normalized, hour_start: new Date(normalized.hour_start).toISOString() });
|
|
129
|
+
rows.set(JSON.stringify([clean.source, clean.model, clean.hour_start]), clean);
|
|
130
|
+
}
|
|
131
|
+
return [...rows.values()];
|
|
132
|
+
}
|
|
133
|
+
async function locked(action) {
|
|
134
|
+
await fs.mkdir(directory, { recursive: true });
|
|
135
|
+
const locks = [];
|
|
136
|
+
try {
|
|
137
|
+
for (const name of ["local-usage.lock", "sync.lock"]) {
|
|
138
|
+
const lock = await openLock(path.join(directory, name), { quietIfLocked: true });
|
|
139
|
+
if (!lock) fail("transfer_busy", 409);
|
|
140
|
+
locks.push(lock);
|
|
141
|
+
}
|
|
142
|
+
return await action();
|
|
143
|
+
} finally { for (const lock of locks.reverse()) await lock.release(); }
|
|
144
|
+
}
|
|
145
|
+
async function write(value) {
|
|
146
|
+
value.revision++;
|
|
147
|
+
if (!Number.isSafeInteger(value.revision)) fail("invalid_state", 503);
|
|
148
|
+
const contents = JSON.stringify(value) + "\n";
|
|
149
|
+
if (Buffer.byteLength(contents) > MAX_STATE_BYTES) fail("too_many_devices");
|
|
150
|
+
await writeFileAtomic(statePath, contents, { mode: 0o600 });
|
|
151
|
+
cachedRaw = undefined;
|
|
152
|
+
cachedState = undefined;
|
|
153
|
+
}
|
|
154
|
+
function self() {
|
|
155
|
+
const machine = identity();
|
|
156
|
+
if (!validId(machine?.id)) fail("identity_unavailable", 503);
|
|
157
|
+
return { id: machine.id, name: String(machine.name || machine.id).slice(0, 128), platform: String(machine.platform || "").slice(0, 30) };
|
|
158
|
+
}
|
|
159
|
+
function imported(value) {
|
|
160
|
+
if (importedCache.has(value)) return importedCache.get(value);
|
|
161
|
+
const entries = Object.entries(value.imports).map(([id, entry]) => {
|
|
162
|
+
const snapshot = validateSnapshot(entry.snapshot);
|
|
163
|
+
if (snapshot.machine.id !== id || !validTime(entry.imported_at)) fail("invalid_state", 503);
|
|
164
|
+
return { ...entry, snapshot };
|
|
165
|
+
});
|
|
166
|
+
importedCache.set(value, entries);
|
|
167
|
+
return entries;
|
|
168
|
+
}
|
|
169
|
+
function inspect(snapshot, value, rows) {
|
|
170
|
+
if (snapshot.machine.id === self().id) fail("self_import");
|
|
171
|
+
const previous = Object.hasOwn(value.imports, snapshot.machine.id) ? value.imports[snapshot.machine.id].snapshot : null;
|
|
172
|
+
const same = previous && snapshot.sequence === previous.sequence;
|
|
173
|
+
if (same && digest(snapshot) !== digest(previous)) fail("snapshot_conflict", 409);
|
|
174
|
+
const action = !previous ? "added" : same ? "unchanged" : snapshot.sequence < previous.sequence ? "outdated" : "updated";
|
|
175
|
+
const entries = imported(value);
|
|
176
|
+
const own = rows.filter((row) => !isAccountLevelSource(row.source));
|
|
177
|
+
const before = displayCount([...own, ...entries.flatMap((entry) => entry.snapshot.rows)]);
|
|
178
|
+
const after = ["added", "updated"].includes(action) ? displayCount([...own, ...snapshot.rows,
|
|
179
|
+
...entries.filter((entry) => entry.snapshot.machine.id !== snapshot.machine.id).flatMap((entry) => entry.snapshot.rows)]) : before;
|
|
180
|
+
return { action, before, after, machine: snapshot.machine, exported_at: snapshot.exported_at,
|
|
181
|
+
token: digest([snapshot, value.revision, rows]) };
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
async exportSnapshot() {
|
|
185
|
+
return locked(async () => {
|
|
186
|
+
const value = structuredClone(await state());
|
|
187
|
+
const rows = (await ownRows()).filter((row) => !isAccountLevelSource(row.source));
|
|
188
|
+
const snapshot = validateSnapshot({ schema: SCHEMA, scope: "device-history", machine: self(),
|
|
189
|
+
sequence: value.sequence + 1, exported_at: new Date().toISOString(), rows });
|
|
190
|
+
value.sequence++;
|
|
191
|
+
await write(value);
|
|
192
|
+
return snapshot;
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
async preview(input) {
|
|
196
|
+
const snapshot = validateSnapshot(input);
|
|
197
|
+
return locked(async () => inspect(snapshot, await state(), await ownRows()));
|
|
198
|
+
},
|
|
199
|
+
async importSnapshot(input, token) {
|
|
200
|
+
const snapshot = validateSnapshot(input);
|
|
201
|
+
return locked(async () => {
|
|
202
|
+
const value = structuredClone(await state());
|
|
203
|
+
const preview = inspect(snapshot, value, await ownRows());
|
|
204
|
+
if (preview.token !== token) fail("preview_changed", 409);
|
|
205
|
+
if (["added", "updated"].includes(preview.action)) {
|
|
206
|
+
if (preview.action === "added" && Object.keys(value.imports).length >= MAX_DEVICES) fail("too_many_devices");
|
|
207
|
+
value.imports = { ...value.imports, [snapshot.machine.id]: { snapshot, imported_at: new Date().toISOString() } };
|
|
208
|
+
await write(value);
|
|
209
|
+
}
|
|
210
|
+
return preview;
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
async remove(id) {
|
|
214
|
+
if (!validId(id)) fail("unknown_device", 404);
|
|
215
|
+
return locked(async () => {
|
|
216
|
+
const value = structuredClone(await state());
|
|
217
|
+
if (!Object.hasOwn(value.imports, id)) fail("unknown_device", 404);
|
|
218
|
+
delete value.imports[id];
|
|
219
|
+
await write(value);
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
async hasImports() { return imported(await state()).length > 0; },
|
|
223
|
+
async view(rows, device = "") {
|
|
224
|
+
const value = await state();
|
|
225
|
+
if (!device) {
|
|
226
|
+
const entries = imported(value);
|
|
227
|
+
if (!entries.length) return rows;
|
|
228
|
+
if (cachedView?.own === rows && cachedView.state === value) return cachedView.rows;
|
|
229
|
+
const combined = [...rows, ...entries.flatMap((entry) => entry.snapshot.rows)];
|
|
230
|
+
cachedView = { own: rows, state: value, rows: combined };
|
|
231
|
+
return combined;
|
|
232
|
+
}
|
|
233
|
+
if (device === `local:${self().id}`) return rows.filter((row) => !isAccountLevelSource(row.source));
|
|
234
|
+
const entry = imported(value).find((item) => device === `local:${item.snapshot.machine.id}`);
|
|
235
|
+
if (!entry) fail("unknown_device", 404);
|
|
236
|
+
return entry.snapshot.rows;
|
|
237
|
+
},
|
|
238
|
+
async devices(rows, inRange) {
|
|
239
|
+
const value = await state();
|
|
240
|
+
const machine = self();
|
|
241
|
+
const entries = imported(value);
|
|
242
|
+
const rangeTotals = (rows) => {
|
|
243
|
+
const selected = rows.filter(inRange);
|
|
244
|
+
return { total_tokens: count(selected), billable_total_tokens: count(selected, "billable_total_tokens") };
|
|
245
|
+
};
|
|
246
|
+
const devices = [{ id: `local:${machine.id}`, machine_id: machine.id, device_name: machine.name,
|
|
247
|
+
platform: machine.platform, isCurrent: true,
|
|
248
|
+
...rangeTotals(rows.filter((row) => !isAccountLevelSource(row.source))) },
|
|
249
|
+
...entries.map(({ snapshot, imported_at }) => ({ id: `local:${snapshot.machine.id}`, machine_id: snapshot.machine.id,
|
|
250
|
+
device_name: snapshot.machine.name, platform: snapshot.machine.platform,
|
|
251
|
+
...rangeTotals(snapshot.rows), imported_at, exported_at: snapshot.exported_at }))];
|
|
252
|
+
const accounts = new Map();
|
|
253
|
+
for (const row of rows.filter((row) => isAccountLevelSource(row.source) && inRange(row))) {
|
|
254
|
+
const totals = accounts.get(row.source) || { total_tokens: 0, billable_total_tokens: 0 };
|
|
255
|
+
totals.total_tokens += row.total_tokens;
|
|
256
|
+
totals.billable_total_tokens += row.billable_total_tokens ?? row.total_tokens;
|
|
257
|
+
accounts.set(row.source, totals);
|
|
258
|
+
}
|
|
259
|
+
const names = devices.map((device) => device.device_name);
|
|
260
|
+
for (const device of devices) {
|
|
261
|
+
if (names.filter((name) => name === device.device_name).length > 1) {
|
|
262
|
+
device.device_name += ` · ${device.machine_id.slice(-8)}`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { revision: value.revision, current_device_id: `local:${machine.id}`, devices,
|
|
266
|
+
account_sources: [...accounts].map(([source, totals]) => ({ source, ...totals })) };
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = { createLocalUsageTransfer, MAX_BYTES };
|
|
@@ -2,9 +2,15 @@ const crypto = require("node:crypto");
|
|
|
2
2
|
const fssync = require("node:fs");
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
const { readSqliteJsonRowsAsync } = require("./sqlite-reader");
|
|
5
|
+
const { readStoredApiKey } = require("./provider-api-key-store");
|
|
5
6
|
|
|
6
7
|
// OpenCode Go usage limits.
|
|
7
8
|
//
|
|
9
|
+
// The API key comes from OPENCODE_GO_API_KEY when set, otherwise from the key
|
|
10
|
+
// saved in the Limits panel (provider-api-key-store.js). The stored key exists
|
|
11
|
+
// so users never need a machine-wide `launchctl setenv`, which reaches every
|
|
12
|
+
// launchd app on macOS and not just TokenTracker (#837).
|
|
13
|
+
//
|
|
8
14
|
// Three data sources are available, in priority order:
|
|
9
15
|
//
|
|
10
16
|
// 1. The official OpenCode Go usage API
|
|
@@ -732,13 +738,17 @@ async function scrapeOpencodeGoWeb({ cfg, fetchImpl, nowMs, timeoutMs }) {
|
|
|
732
738
|
|
|
733
739
|
async function fetchOpencodeGoLimits({
|
|
734
740
|
home,
|
|
741
|
+
trackerDir,
|
|
735
742
|
env = process.env,
|
|
736
743
|
fetchImpl = fetch,
|
|
737
744
|
nowMs = Date.now(),
|
|
738
745
|
timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
739
746
|
sqliteOptions = {},
|
|
740
747
|
} = {}) {
|
|
741
|
-
const
|
|
748
|
+
const envApiKey = readApiKey(env);
|
|
749
|
+
const apiKey =
|
|
750
|
+
envApiKey || readStoredApiKey({ provider: "opencode-go", trackerDir, home });
|
|
751
|
+
const credentialSource = envApiKey ? "env" : apiKey ? "stored" : null;
|
|
742
752
|
const cfg = readConfig(env);
|
|
743
753
|
const allowLocalEstimate = localEstimateEnabled(env);
|
|
744
754
|
|
|
@@ -748,13 +758,14 @@ async function fetchOpencodeGoLimits({
|
|
|
748
758
|
// existing cookie remains a compatibility fallback.
|
|
749
759
|
if (apiKey) {
|
|
750
760
|
const api = await fetchOpencodeGoApiLimits({ apiKey, fetchImpl, nowMs, timeoutMs });
|
|
751
|
-
|
|
752
|
-
|
|
761
|
+
const sourcedApi = credentialSource ? { ...api, credential_source: credentialSource } : api;
|
|
762
|
+
if (!sourcedApi?.error && (sourcedApi.primary_window || sourcedApi.secondary_window || sourcedApi.tertiary_window)) {
|
|
763
|
+
return { ...sourcedApi, source: "api" };
|
|
753
764
|
}
|
|
754
|
-
if (
|
|
755
|
-
return { ...
|
|
765
|
+
if (sourcedApi?.auth_error) {
|
|
766
|
+
return { ...sourcedApi, source: "api" };
|
|
756
767
|
}
|
|
757
|
-
if (!cfg) return
|
|
768
|
+
if (!cfg) return sourcedApi || { configured: true, error: "OpenCode Go unavailable" };
|
|
758
769
|
}
|
|
759
770
|
|
|
760
771
|
// Keep the cookie-backed dashboard path for existing configurations and as a
|
|
@@ -89,12 +89,27 @@
|
|
|
89
89
|
"cache_write": 1.25,
|
|
90
90
|
"note": "Canonical undated alias emitted by Cursor. Pin to the same rates as claude-haiku-4-5-20251001 so local cost does not depend on which dated LiteLLM aliases are present in the current cache."
|
|
91
91
|
},
|
|
92
|
+
"claude-haiku-5-5": {
|
|
93
|
+
"input": 0.1,
|
|
94
|
+
"output": 0.5,
|
|
95
|
+
"cache_read": 0.01,
|
|
96
|
+
"cache_write": 0.125,
|
|
97
|
+
"note": "Official Anthropic Haiku 5.5 short-context (prompts <=100K) USD/MTok verified 2026-10-22: https://platform.claude.com/docs/en/about-claude/pricing. Prompts over 100K use 5x rates; computeRowCost does not yet split that long-prompt tier. Cache write is the 5-minute rate."
|
|
98
|
+
},
|
|
99
|
+
"claude-haiku-5.5": {
|
|
100
|
+
"input": 0.1,
|
|
101
|
+
"output": 0.5,
|
|
102
|
+
"cache_read": 0.01,
|
|
103
|
+
"cache_write": 0.125,
|
|
104
|
+
"note": "Dotted alias for claude-haiku-5-5. Same short-context rates."
|
|
105
|
+
},
|
|
92
106
|
"gpt-6-astra": {
|
|
93
107
|
"input": 10,
|
|
94
108
|
"output": 50,
|
|
95
109
|
"cache_read": 1,
|
|
96
110
|
"cache_write": 12.5,
|
|
97
111
|
"priority_multiplier": 2,
|
|
112
|
+
"ultrafast_multiplier": 6,
|
|
98
113
|
"note": "Official Standard USD/MTok rates verified 2026-09-07: https://developers.openai.com/api/docs/models/gpt-6-astra. Requests above 272K input tokens use 2x input/cache and 1.5x output rates; computeRowCost applies this only to observed long-context request subsets. priority_multiplier is the Fast tier, whose published table (20/2/25/100 short, 40/4/50/150 long, verified 2026-09-12) is exactly 2x the Standard rate of the same context tier — it multiplies, it does not replace, the long-context premium. The 2.5x figure in the Codex speed docs is a subscription-credit rate and must not be used here. computeRowCost applies it only to observed priority request subsets; usage whose service tier the logs never recorded stays Standard. Cloud aggregates use Standard short-context estimates because they do not retain per-request context or service tier. Reasoning-effort suffixes use the same base rates."
|
|
99
114
|
},
|
|
100
115
|
"gpt-6.1-sol": {
|
|
@@ -103,6 +118,7 @@
|
|
|
103
118
|
"cache_read": 0.1,
|
|
104
119
|
"cache_write": 2.5,
|
|
105
120
|
"priority_multiplier": 2,
|
|
121
|
+
"ultrafast_multiplier": 6,
|
|
106
122
|
"note": "Official Standard USD/MTok rates verified 2026-10-02 (issue #737): https://developers.openai.com/api/docs/models/gpt-6.1-sol. Requests above 272K input use 2x input/cache and 1.5x output rates; Fast mode is 2x the applicable rates. Premiums apply only to observed request subsets. Cloud aggregates remain Standard short-context estimates."
|
|
107
123
|
},
|
|
108
124
|
"gpt-6-sol": {
|
|
@@ -682,6 +698,14 @@
|
|
|
682
698
|
"match": "composer",
|
|
683
699
|
"ref": "composer-1"
|
|
684
700
|
},
|
|
701
|
+
{
|
|
702
|
+
"match": "haiku-5-5",
|
|
703
|
+
"ref": "claude-haiku-5-5"
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"match": "haiku-5.5",
|
|
707
|
+
"ref": "claude-haiku-5.5"
|
|
708
|
+
},
|
|
685
709
|
{
|
|
686
710
|
"match": "claude-opus-5-fast",
|
|
687
711
|
"ref": "claude-opus-5-fast"
|
package/src/lib/pricing/index.js
CHANGED
|
@@ -273,7 +273,9 @@ function computeRowCost(row) {
|
|
|
273
273
|
// and inventing one is worse than reporting Standard.
|
|
274
274
|
const priorityMultiplier = Number(pricing.priority_multiplier);
|
|
275
275
|
const hasPriorityRates = Number.isFinite(priorityMultiplier) && priorityMultiplier > 1;
|
|
276
|
-
|
|
276
|
+
const ultrafastMultiplierEarly = Number(pricing.ultrafast_multiplier);
|
|
277
|
+
const hasUltrafastRatesEarly = Number.isFinite(ultrafastMultiplierEarly) && ultrafastMultiplierEarly > 1;
|
|
278
|
+
if (!usesOpenAILongContextTier && !hasPriorityRates && !hasUltrafastRatesEarly) return baseCost;
|
|
277
279
|
|
|
278
280
|
const bounded = (value, total) => Math.min(
|
|
279
281
|
Math.max(0, Number(value) || 0),
|
|
@@ -306,7 +308,49 @@ function computeRowCost(row) {
|
|
|
306
308
|
0.5 * longReasoning * (pricing.output || 0)
|
|
307
309
|
) / 1_000_000 : 0;
|
|
308
310
|
|
|
309
|
-
|
|
311
|
+
const ultrafastMultiplier = ultrafastMultiplierEarly;
|
|
312
|
+
const hasUltrafastRates = hasUltrafastRatesEarly;
|
|
313
|
+
if (!hasPriorityRates && !hasUltrafastRates) return baseCost + longContextPremium;
|
|
314
|
+
|
|
315
|
+
// Shared helper: premium for one observed speed-tier subset. Fast and Ultrafast
|
|
316
|
+
// are mutually exclusive per request; both can appear in one aggregated row.
|
|
317
|
+
function tierPremium(multiplier, prefix) {
|
|
318
|
+
if (!(Number.isFinite(multiplier) && multiplier > 1)) return 0;
|
|
319
|
+
const tierInput = bounded(row[`${prefix}input_tokens`], row.input_tokens);
|
|
320
|
+
const tierCached = bounded(row[`${prefix}cached_input_tokens`], row.cached_input_tokens);
|
|
321
|
+
const tierCacheWrite = bounded(
|
|
322
|
+
row[`${prefix}cache_creation_input_tokens`],
|
|
323
|
+
row.cache_creation_input_tokens,
|
|
324
|
+
);
|
|
325
|
+
const tierOutput = bounded(row[`${prefix}output_tokens`], row.output_tokens);
|
|
326
|
+
const tierReasoning = reasoningIncludedInOutput
|
|
327
|
+
? 0
|
|
328
|
+
: bounded(row[`${prefix}reasoning_output_tokens`], row.reasoning_output_tokens);
|
|
329
|
+
const tierBase = (
|
|
330
|
+
tierInput * (pricing.input || 0) +
|
|
331
|
+
tierCached * (pricing.cache_read || 0) +
|
|
332
|
+
tierCacheWrite * (pricing.cache_write || 0) +
|
|
333
|
+
tierOutput * (pricing.output || 0) +
|
|
334
|
+
tierReasoning * (pricing.output || 0)
|
|
335
|
+
) / 1_000_000;
|
|
336
|
+
const tierLong = usesOpenAILongContextTier ? (
|
|
337
|
+
bounded(row[`${prefix}long_context_input_tokens`], Math.min(tierInput, longInput)) *
|
|
338
|
+
(pricing.input || 0) +
|
|
339
|
+
bounded(row[`${prefix}long_context_cached_input_tokens`], Math.min(tierCached, longCached)) *
|
|
340
|
+
(pricing.cache_read || 0) +
|
|
341
|
+
bounded(
|
|
342
|
+
row[`${prefix}long_context_cache_creation_input_tokens`],
|
|
343
|
+
Math.min(tierCacheWrite, longCacheWrite),
|
|
344
|
+
) * (pricing.cache_write || 0) +
|
|
345
|
+
0.5 * bounded(row[`${prefix}long_context_output_tokens`], Math.min(tierOutput, longOutput)) *
|
|
346
|
+
(pricing.output || 0) +
|
|
347
|
+
0.5 * bounded(
|
|
348
|
+
row[`${prefix}long_context_reasoning_output_tokens`],
|
|
349
|
+
Math.min(tierReasoning, longReasoning),
|
|
350
|
+
) * (pricing.output || 0)
|
|
351
|
+
) / 1_000_000 : 0;
|
|
352
|
+
return (multiplier - 1) * (tierBase + tierLong);
|
|
353
|
+
}
|
|
310
354
|
|
|
311
355
|
// Astra Fast is a flat multiple of whichever context tier the request was on
|
|
312
356
|
// (short 10/1/12.5/50 -> 20/2/25/100; long 20/2/25/75 -> 40/4/50/150), so the
|
|
@@ -316,43 +360,10 @@ function computeRowCost(row) {
|
|
|
316
360
|
// priority_long_context_* alongside priority_*; without it (queue rows, which
|
|
317
361
|
// carry no context-length subset at all) the overlap is 0 and the premium
|
|
318
362
|
// falls back to the short-context multiple, matching the row's own base.
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
row.cache_creation_input_tokens,
|
|
324
|
-
);
|
|
325
|
-
const priorityOutput = bounded(row.priority_output_tokens, row.output_tokens);
|
|
326
|
-
const priorityReasoning = reasoningIncludedInOutput
|
|
327
|
-
? 0
|
|
328
|
-
: bounded(row.priority_reasoning_output_tokens, row.reasoning_output_tokens);
|
|
329
|
-
const priorityBase = (
|
|
330
|
-
priorityInput * (pricing.input || 0) +
|
|
331
|
-
priorityCached * (pricing.cache_read || 0) +
|
|
332
|
-
priorityCacheWrite * (pricing.cache_write || 0) +
|
|
333
|
-
priorityOutput * (pricing.output || 0) +
|
|
334
|
-
priorityReasoning * (pricing.output || 0)
|
|
335
|
-
) / 1_000_000;
|
|
336
|
-
|
|
337
|
-
const priorityLong = usesOpenAILongContextTier ? (
|
|
338
|
-
bounded(row.priority_long_context_input_tokens, Math.min(priorityInput, longInput)) *
|
|
339
|
-
(pricing.input || 0) +
|
|
340
|
-
bounded(row.priority_long_context_cached_input_tokens, Math.min(priorityCached, longCached)) *
|
|
341
|
-
(pricing.cache_read || 0) +
|
|
342
|
-
bounded(
|
|
343
|
-
row.priority_long_context_cache_creation_input_tokens,
|
|
344
|
-
Math.min(priorityCacheWrite, longCacheWrite),
|
|
345
|
-
) * (pricing.cache_write || 0) +
|
|
346
|
-
0.5 * bounded(row.priority_long_context_output_tokens, Math.min(priorityOutput, longOutput)) *
|
|
347
|
-
(pricing.output || 0) +
|
|
348
|
-
0.5 * bounded(
|
|
349
|
-
row.priority_long_context_reasoning_output_tokens,
|
|
350
|
-
Math.min(priorityReasoning, longReasoning),
|
|
351
|
-
) * (pricing.output || 0)
|
|
352
|
-
) / 1_000_000 : 0;
|
|
353
|
-
|
|
354
|
-
const priorityPremium = (priorityMultiplier - 1) * (priorityBase + priorityLong);
|
|
355
|
-
return baseCost + longContextPremium + priorityPremium;
|
|
363
|
+
// Ultrafast mirrors the same shape with ultrafast_* / ultrafast_multiplier (6x).
|
|
364
|
+
const priorityPremium = tierPremium(priorityMultiplier, "priority_");
|
|
365
|
+
const ultrafastPremium = tierPremium(ultrafastMultiplier, "ultrafast_");
|
|
366
|
+
return baseCost + longContextPremium + priorityPremium + ultrafastPremium;
|
|
356
367
|
}
|
|
357
368
|
|
|
358
369
|
// Backwards-compatible MODEL_PRICING export. Test at
|
|
@@ -280,6 +280,15 @@ function lookupPricing(model, { curated, litellm, source } = {}) {
|
|
|
280
280
|
value: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
|
+
// OpenCode Zen free models carry a `-free` suffix on the paid model id
|
|
284
|
+
// (`deepseek-v4.1-flash-free`); the base-name fuzzy match must not bill them.
|
|
285
|
+
if (sourceKey === "opencode" && lower.endsWith("-free")) {
|
|
286
|
+
return {
|
|
287
|
+
hit: true,
|
|
288
|
+
source: "curated:opencode-free-suffix",
|
|
289
|
+
value: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
283
292
|
// AStudio does not disclose its routed model. Stop before generic aliases
|
|
284
293
|
// and fuzzy matching can turn an unresolved router into a priced model.
|
|
285
294
|
if (sourceKey === "acode" && (lower === "auto" || lower.endsWith("-auto"))) {
|