@ipv9/tokentracker-cli 0.39.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.
Files changed (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +464 -0
  3. package/README.ko.md +464 -0
  4. package/README.md +476 -0
  5. package/README.zh-CN.md +469 -0
  6. package/bin/tracker.js +52 -0
  7. package/dashboard/dist/app-icon.png +0 -0
  8. package/dashboard/dist/apple-touch-icon.png +0 -0
  9. package/dashboard/dist/assets/ActivityHeatmap-BxBGHY1z.js +42 -0
  10. package/dashboard/dist/assets/Card-GROxUc_6.js +1 -0
  11. package/dashboard/dist/assets/DashboardPage-BZ5VFoBk.js +19 -0
  12. package/dashboard/dist/assets/DevicePage-CQts7ivB.js +1 -0
  13. package/dashboard/dist/assets/DialogTitle-DqFsk26U.js +1 -0
  14. package/dashboard/dist/assets/FadeIn-HBBXW_3q.js +1 -0
  15. package/dashboard/dist/assets/HeaderGithubStar-COD2HfaF.js +1 -0
  16. package/dashboard/dist/assets/IpCheckPage-Cn0l8-BQ.js +15 -0
  17. package/dashboard/dist/assets/LandingPage-CmV1BMU4.js +4356 -0
  18. package/dashboard/dist/assets/LeaderboardAvatar-CiIpWZwb.js +1 -0
  19. package/dashboard/dist/assets/LeaderboardPage-BU0mlR3g.js +6 -0
  20. package/dashboard/dist/assets/LeaderboardProfileModal-C49_3Xm9.js +72 -0
  21. package/dashboard/dist/assets/LeaderboardProfilePage-Bh1n0BAM.js +1 -0
  22. package/dashboard/dist/assets/LimitsPage-fZAmwLWT.js +2 -0
  23. package/dashboard/dist/assets/LocalOnlyNotice-D3rQdA-P.js +1 -0
  24. package/dashboard/dist/assets/LoginPage-enO4XP-c.js +1 -0
  25. package/dashboard/dist/assets/PopoverPopup-pitJEpN1.js +1 -0
  26. package/dashboard/dist/assets/Select-tT9x4ntD.js +1 -0
  27. package/dashboard/dist/assets/SelectItemText-C7F7eIm0.js +1 -0
  28. package/dashboard/dist/assets/SettingsPage-pDasZNU2.js +1 -0
  29. package/dashboard/dist/assets/SkillsPage-DEu7FY-2.js +1 -0
  30. package/dashboard/dist/assets/WidgetsPage-DGcO7rbp.js +1 -0
  31. package/dashboard/dist/assets/WrappedPage-D_1ID213.js +1 -0
  32. package/dashboard/dist/assets/agent-logos-DXQVg1xy.js +1 -0
  33. package/dashboard/dist/assets/arrow-up-right-DNLo92Ba.js +1 -0
  34. package/dashboard/dist/assets/download-DbBBBpJF.js +1 -0
  35. package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-BPBWmzPh.woff +0 -0
  36. package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-Ce5q_31Z.woff2 +0 -0
  37. package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-CJBLNVQT.woff2 +0 -0
  38. package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-mNhfPmgl.woff +0 -0
  39. package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-DH5Q319x.woff +0 -0
  40. package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-VCNRadI3.woff2 +0 -0
  41. package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-BKC3PZkM.woff +0 -0
  42. package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-DdrQ8KBw.woff2 +0 -0
  43. package/dashboard/dist/assets/geist-mono-latin-400-normal-CoULgQGM.woff +0 -0
  44. package/dashboard/dist/assets/geist-mono-latin-400-normal-LC9RFr9I.woff2 +0 -0
  45. package/dashboard/dist/assets/geist-mono-latin-500-normal-D3o2eNa9.woff2 +0 -0
  46. package/dashboard/dist/assets/geist-mono-latin-500-normal-DOxI7kZ4.woff +0 -0
  47. package/dashboard/dist/assets/geist-mono-latin-700-normal-D6izGJRP.woff2 +0 -0
  48. package/dashboard/dist/assets/geist-mono-latin-700-normal-QGw08Lff.woff +0 -0
  49. package/dashboard/dist/assets/geist-mono-latin-900-normal-CmoKXrdK.woff +0 -0
  50. package/dashboard/dist/assets/geist-mono-latin-900-normal-Cu5MFKsu.woff2 +0 -0
  51. package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-Cgks_Qgx.woff2 +0 -0
  52. package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-CxNRRMGd.woff +0 -0
  53. package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-CQcGuCNt.woff2 +0 -0
  54. package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-diTenJ8L.woff +0 -0
  55. package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-BX9f1BHp.woff +0 -0
  56. package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-YOllDaLV.woff2 +0 -0
  57. package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-Br6WNUGb.woff +0 -0
  58. package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-CrmBTzU2.woff2 +0 -0
  59. package/dashboard/dist/assets/index-BeoRn2gJ.js +2 -0
  60. package/dashboard/dist/assets/info-Bh5qTz6k.js +1 -0
  61. package/dashboard/dist/assets/main-BIY3Nj6I.js +969 -0
  62. package/dashboard/dist/assets/main-G_F9Iu_x.css +1 -0
  63. package/dashboard/dist/assets/use-limits-display-prefs-B4iJ-bBc.js +1 -0
  64. package/dashboard/dist/assets/use-native-settings-DHjPFDv8.js +1 -0
  65. package/dashboard/dist/assets/use-usage-limits-odqe9OY7.js +1 -0
  66. package/dashboard/dist/assets/useCurrency-BsSZiZMP.js +1 -0
  67. package/dashboard/dist/assets/useOpenInteractionType-BQhgq4rv.js +12 -0
  68. package/dashboard/dist/brand-logos/antigravity.svg +1 -0
  69. package/dashboard/dist/brand-logos/claude-code.svg +1 -0
  70. package/dashboard/dist/brand-logos/codebuddy.svg +1 -0
  71. package/dashboard/dist/brand-logos/codex.svg +15 -0
  72. package/dashboard/dist/brand-logos/copilot.svg +1 -0
  73. package/dashboard/dist/brand-logos/cursor.svg +1 -0
  74. package/dashboard/dist/brand-logos/gemini.svg +1 -0
  75. package/dashboard/dist/brand-logos/grok.svg +1 -0
  76. package/dashboard/dist/brand-logos/hermes.svg +1 -0
  77. package/dashboard/dist/brand-logos/kilo.svg +4 -0
  78. package/dashboard/dist/brand-logos/kimi.svg +1 -0
  79. package/dashboard/dist/brand-logos/kiro.svg +1 -0
  80. package/dashboard/dist/brand-logos/openclaw.svg +22 -0
  81. package/dashboard/dist/brand-logos/opencode.svg +3 -0
  82. package/dashboard/dist/clawd/happy.svg +161 -0
  83. package/dashboard/dist/clawd/idle/collapse.svg +101 -0
  84. package/dashboard/dist/clawd/idle/doze.svg +72 -0
  85. package/dashboard/dist/clawd/idle/follow.svg +64 -0
  86. package/dashboard/dist/clawd/idle/living.svg +196 -0
  87. package/dashboard/dist/clawd/idle/look.svg +115 -0
  88. package/dashboard/dist/clawd/idle/yawn.svg +158 -0
  89. package/dashboard/dist/clawd/mini/alert.svg +129 -0
  90. package/dashboard/dist/clawd/mini/crabwalk.svg +76 -0
  91. package/dashboard/dist/clawd/mini/enter-sleep.svg +65 -0
  92. package/dashboard/dist/clawd/mini/enter.svg +115 -0
  93. package/dashboard/dist/clawd/mini/happy.svg +124 -0
  94. package/dashboard/dist/clawd/mini/idle-tight.svg +15 -0
  95. package/dashboard/dist/clawd/mini/idle.svg +83 -0
  96. package/dashboard/dist/clawd/mini/peek.svg +82 -0
  97. package/dashboard/dist/clawd/mini/sleep.svg +112 -0
  98. package/dashboard/dist/clawd/react/double.svg +108 -0
  99. package/dashboard/dist/clawd/react/drag.svg +102 -0
  100. package/dashboard/dist/clawd/react/left.svg +102 -0
  101. package/dashboard/dist/clawd/react/right.svg +102 -0
  102. package/dashboard/dist/clawd/sleep/collapse-sleep.svg +247 -0
  103. package/dashboard/dist/clawd/sleep/sleeping.svg +118 -0
  104. package/dashboard/dist/clawd/sleep/wake.svg +277 -0
  105. package/dashboard/dist/clawd/static-base.svg +21 -0
  106. package/dashboard/dist/clawd/status/disconnected.svg +136 -0
  107. package/dashboard/dist/clawd/status/error.svg +94 -0
  108. package/dashboard/dist/clawd/status/notification.svg +134 -0
  109. package/dashboard/dist/clawd/working/building.svg +231 -0
  110. package/dashboard/dist/clawd/working/carrying.svg +107 -0
  111. package/dashboard/dist/clawd/working/conducting.svg +118 -0
  112. package/dashboard/dist/clawd/working/confused.svg +113 -0
  113. package/dashboard/dist/clawd/working/debugger.svg +91 -0
  114. package/dashboard/dist/clawd/working/juggling.svg +82 -0
  115. package/dashboard/dist/clawd/working/overheated.svg +75 -0
  116. package/dashboard/dist/clawd/working/pushing.svg +125 -0
  117. package/dashboard/dist/clawd/working/sweeping.svg +79 -0
  118. package/dashboard/dist/clawd/working/thinking.svg +119 -0
  119. package/dashboard/dist/clawd/working/typing.svg +153 -0
  120. package/dashboard/dist/clawd/working/ultrathink.svg +166 -0
  121. package/dashboard/dist/clawd/working/wizard.svg +98 -0
  122. package/dashboard/dist/dashboard-dark.png +0 -0
  123. package/dashboard/dist/favicon-16.png +0 -0
  124. package/dashboard/dist/favicon-32.png +0 -0
  125. package/dashboard/dist/favicon.ico +0 -0
  126. package/dashboard/dist/feed.xml +34 -0
  127. package/dashboard/dist/icon-192.png +0 -0
  128. package/dashboard/dist/icon-512.png +0 -0
  129. package/dashboard/dist/index.html +355 -0
  130. package/dashboard/dist/llms.txt +53 -0
  131. package/dashboard/dist/neon_bg.png +0 -0
  132. package/dashboard/dist/robots.txt +7 -0
  133. package/dashboard/dist/share.html +140 -0
  134. package/dashboard/dist/sitemap.xml +27 -0
  135. package/dashboard/dist/widgets-overview.png +0 -0
  136. package/package.json +78 -0
  137. package/scripts/install-local-service.sh +140 -0
  138. package/scripts/uninstall-local-service.sh +21 -0
  139. package/src/cli.js +95 -0
  140. package/src/commands/device-login.js +161 -0
  141. package/src/commands/diagnostics.js +38 -0
  142. package/src/commands/doctor.js +96 -0
  143. package/src/commands/init.js +1195 -0
  144. package/src/commands/serve.js +351 -0
  145. package/src/commands/status.js +609 -0
  146. package/src/commands/sync.js +2285 -0
  147. package/src/commands/uninstall.js +189 -0
  148. package/src/commands/wrapped.js +150 -0
  149. package/src/lib/antigravity-paths.js +48 -0
  150. package/src/lib/browser-auth.js +281 -0
  151. package/src/lib/categorizer-utils.js +232 -0
  152. package/src/lib/claude-categorizer.js +1319 -0
  153. package/src/lib/claude-config.js +204 -0
  154. package/src/lib/cli-ui.js +179 -0
  155. package/src/lib/codex-config.js +350 -0
  156. package/src/lib/codex-context-breakdown.js +400 -0
  157. package/src/lib/codex-rollout-parser.js +623 -0
  158. package/src/lib/codex-token-refresh.js +112 -0
  159. package/src/lib/cursor-config.js +425 -0
  160. package/src/lib/debug-flags.js +7 -0
  161. package/src/lib/diagnostics.js +272 -0
  162. package/src/lib/doctor.js +338 -0
  163. package/src/lib/fs.js +91 -0
  164. package/src/lib/gemini-config.js +288 -0
  165. package/src/lib/grok-hook.js +351 -0
  166. package/src/lib/init-flow.js +42 -0
  167. package/src/lib/local-api.js +1861 -0
  168. package/src/lib/openclaw-hook.js +420 -0
  169. package/src/lib/openclaw-session-plugin.js +520 -0
  170. package/src/lib/opencode-config.js +99 -0
  171. package/src/lib/passive-mode.js +185 -0
  172. package/src/lib/pricing/curated-overrides.json +79 -0
  173. package/src/lib/pricing/index.js +143 -0
  174. package/src/lib/pricing/litellm-fetcher.js +172 -0
  175. package/src/lib/pricing/matcher.js +255 -0
  176. package/src/lib/pricing/seed-snapshot.json +1 -0
  177. package/src/lib/progress.js +76 -0
  178. package/src/lib/project-usage-purge.js +106 -0
  179. package/src/lib/prompt.js +24 -0
  180. package/src/lib/proxy-env.js +131 -0
  181. package/src/lib/rollout.js +8442 -0
  182. package/src/lib/runtime-config.js +122 -0
  183. package/src/lib/skill-usage.js +267 -0
  184. package/src/lib/skills-manager.js +1127 -0
  185. package/src/lib/source-metadata.js +48 -0
  186. package/src/lib/sqlite-reader.js +136 -0
  187. package/src/lib/static-server.js +59 -0
  188. package/src/lib/subscriptions.js +453 -0
  189. package/src/lib/tracker-paths.js +16 -0
  190. package/src/lib/upload-throttle.js +148 -0
  191. package/src/lib/usage-limits.js +1806 -0
  192. package/src/lib/wrapped-aggregator.js +225 -0
@@ -0,0 +1,255 @@
1
+ // Pure pricing-lookup logic. No I/O, no async. Tested in isolation.
2
+ //
3
+ // Resolve order:
4
+ // 1. CURATED exact match (self-defined aliases like kiro-*, hy3-*)
5
+ // 2. LiteLLM exact match (mainstream claude/gpt-5/gemini)
6
+ // 3. CURATED alias (e.g. "auto" -> "composer-1")
7
+ // 4. CURATED fuzzy substring (e.g. "kiro-future-xyz" matches via "kiro")
8
+ // 5. LiteLLM suffix-strip (gpt-5-codex-high-fast -> gpt-5-codex)
9
+ // 5b. LiteLLM provider-prefix strip (mimo-v2.5-pro -> openrouter/xiaomi/mimo-v2.5-pro)
10
+ // 6. LiteLLM reverse substring (longest-key first)
11
+ // 7. null (caller decides what to do — typically zero-pricing + negative cache)
12
+
13
+ const SUFFIX_STRIP_PATTERNS = [
14
+ /-xhigh-fast$/,
15
+ /-high-fast$/,
16
+ /-medium-fast$/,
17
+ /-low-fast$/,
18
+ /-xhigh$/,
19
+ /-high$/,
20
+ /-medium$/,
21
+ /-low$/,
22
+ /-fast$/,
23
+ ];
24
+
25
+ function stripReasoningSuffix(model) {
26
+ for (const re of SUFFIX_STRIP_PATTERNS) {
27
+ if (re.test(model)) return model.replace(re, "");
28
+ }
29
+ return model;
30
+ }
31
+
32
+ function normalizeAntigravityModel(model) {
33
+ if (!model || typeof model !== "string") return model;
34
+ let lower = model
35
+ .trim()
36
+ .replace(/\([^)]*\)/g, " ")
37
+ .replace(/\b(thinking|xhigh|high|medium|low|fast)\b/gi, " ")
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9.]+/g, "-")
40
+ .replace(/^-+|-+$/g, "")
41
+ .replace(/-{2,}/g, "-");
42
+ lower = stripReasoningSuffix(lower);
43
+
44
+ if (lower.startsWith("gemini-claude-") || lower.startsWith("gemini-gpt-")) {
45
+ lower = lower.substring(7);
46
+ }
47
+
48
+ if (/^gemini-3\.\d+-flash-lite/.test(lower)) return "gemini-2.5-flash-lite";
49
+ if (/^gemini-3\.\d+-flash/.test(lower)) return "gemini-2.5-flash";
50
+ if (/^gemini-3\.\d+-pro/.test(lower)) return "gemini-2.5-pro";
51
+ if (/^claude-(sonnet|opus|haiku)-4\.\d+/.test(lower)) {
52
+ return lower.replace(/^claude-(sonnet|opus|haiku)-4\.(\d+)/, "claude-$1-4-$2");
53
+ }
54
+ if (lower.startsWith("gpt-oss-120b")) return "antigravity-gpt-oss-120b";
55
+
56
+ return lower;
57
+ }
58
+
59
+ function shouldNormalizeAntigravity(source) {
60
+ return typeof source === "string" && source.toLowerCase() === "antigravity";
61
+ }
62
+
63
+ // Memoise the sorted-by-length LiteLLM key list. Reverse-substring scan walks
64
+ // this once per uncached model; ~2k keys × negligible per-iteration cost, but
65
+ // computing the sort on every call would add up across a sync.
66
+ const sortedKeysCache = new WeakMap();
67
+ function getSortedKeys(litellm) {
68
+ let cached = sortedKeysCache.get(litellm);
69
+ if (!cached) {
70
+ cached = Object.keys(litellm).sort((a, b) => b.length - a.length);
71
+ sortedKeysCache.set(litellm, cached);
72
+ }
73
+ return cached;
74
+ }
75
+
76
+ function buildDotRestoredModel(model) {
77
+ if (typeof model !== "string") return "";
78
+ const lower = model.toLowerCase();
79
+ const restored = lower.replace(/(\d+)-(\d+)/g, "$1.$2");
80
+ return restored === lower ? "" : restored;
81
+ }
82
+
83
+ function lookupExactCaseInsensitive(table, model) {
84
+ if (!table || !model) return null;
85
+ if (table[model]) return table[model];
86
+ const lower = model.toLowerCase();
87
+ for (const key of Object.keys(table)) {
88
+ if (key.toLowerCase() === lower) return table[key];
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function lookupContainedExactCaseInsensitive(table, model) {
94
+ if (!table || !model) return null;
95
+ const lower = model.toLowerCase();
96
+ const keys = Object.keys(table).sort((a, b) => b.length - a.length);
97
+ for (const key of keys) {
98
+ if (lower.includes(key.toLowerCase())) return table[key];
99
+ }
100
+ return null;
101
+ }
102
+
103
+ function lookupPricing(model, { curated, litellm, source } = {}) {
104
+ if (!model || typeof model !== "string") {
105
+ return { hit: false, source: "empty", value: null };
106
+ }
107
+ const lookupModel = shouldNormalizeAntigravity(source)
108
+ ? normalizeAntigravityModel(model)
109
+ : model;
110
+ const lower = lookupModel.toLowerCase();
111
+ const dotForm = buildDotRestoredModel(lookupModel);
112
+
113
+ // 1. CURATED exact
114
+ if (curated.exact && curated.exact[lookupModel]) {
115
+ return { hit: true, source: "curated:exact", value: curated.exact[lookupModel] };
116
+ }
117
+ const curatedDotExact = lookupExactCaseInsensitive(curated.exact, dotForm);
118
+ if (curatedDotExact) {
119
+ return { hit: true, source: "curated:exact-dot", value: curatedDotExact };
120
+ }
121
+ const curatedDotContainedExact = lookupContainedExactCaseInsensitive(curated.exact, dotForm);
122
+ if (curatedDotContainedExact) {
123
+ return { hit: true, source: "curated:exact-dot", value: curatedDotContainedExact };
124
+ }
125
+
126
+ // 2. LiteLLM exact
127
+ if (litellm && litellm[lookupModel]) {
128
+ return { hit: true, source: "litellm:exact", value: litellm[lookupModel] };
129
+ }
130
+ const litellmDotExact = lookupExactCaseInsensitive(litellm, dotForm);
131
+ if (litellmDotExact) {
132
+ return { hit: true, source: "litellm:exact-dot", value: litellmDotExact };
133
+ }
134
+
135
+ // 3. CURATED alias (literal mapping like "auto" -> "composer-1")
136
+ if (curated.alias && curated.alias[lookupModel] && curated.exact[curated.alias[lookupModel]]) {
137
+ return {
138
+ hit: true,
139
+ source: "curated:alias",
140
+ value: curated.exact[curated.alias[lookupModel]],
141
+ };
142
+ }
143
+
144
+ // 4. CURATED fuzzy substring. Also try a dot-restored variant of the input
145
+ // (digits separated by `-` rejoined as `.`) so providers that dash-normalize
146
+ // numeric segments — Droid emits `glm-5-1-0` for upstream `GLM-5.1` — still
147
+ // resolve against dot-keyed curated entries like `glm-5.1`, `glm-4.6`, etc.
148
+ // The regex only fires on digit-dash-digit, so `claude-3-7-sonnet`,
149
+ // `gpt-5-codex`, `gemini-2-5-pro` are unaffected (no digit-pair to rejoin or
150
+ // no matching curated key).
151
+ if (Array.isArray(curated.fuzzy)) {
152
+ for (const { match, ref } of curated.fuzzy) {
153
+ if (!match || !ref) continue;
154
+ const needle = match.toLowerCase();
155
+ if (!curated.exact[ref]) continue;
156
+ if (lower.includes(needle)) {
157
+ return { hit: true, source: "curated:fuzzy", value: curated.exact[ref] };
158
+ }
159
+ if (dotForm && dotForm.includes(needle)) {
160
+ return { hit: true, source: "curated:fuzzy", value: curated.exact[ref] };
161
+ }
162
+ }
163
+ }
164
+
165
+ // 5. LiteLLM suffix-strip
166
+ if (litellm) {
167
+ const stripped = stripReasoningSuffix(lookupModel);
168
+ if (stripped !== lookupModel && litellm[stripped]) {
169
+ return { hit: true, source: "litellm:strip", value: litellm[stripped] };
170
+ }
171
+ }
172
+
173
+ // 5b. LiteLLM provider-prefix strip. Queue rows store the bare model name
174
+ // (e.g. "mimo-v2.5-pro"), but LiteLLM keys are provider-qualified (e.g.
175
+ // "openrouter/xiaomi/mimo-v2.5-pro"), so the exact lookups above miss. Match
176
+ // any key whose path suffix equals the bare model. When several providers
177
+ // expose the same model, pick the lexicographically smallest key so the
178
+ // resolved price is deterministic and independent of JSON ordering. Runs
179
+ // AFTER curated alias/fuzzy so e.g. Cursor's "auto" still resolves to
180
+ // composer-1 rather than a LiteLLM "*/auto" entry.
181
+ if (litellm) {
182
+ const suffix = "/" + lower;
183
+ let best = null;
184
+ for (const key of Object.keys(litellm)) {
185
+ if (key.length > suffix.length && key.toLowerCase().endsWith(suffix)) {
186
+ if (best === null || key < best) best = key;
187
+ }
188
+ }
189
+ if (best) return { hit: true, source: "litellm:prefix-strip", value: litellm[best] };
190
+ }
191
+
192
+ // 6. LiteLLM reverse substring (longest-key first)
193
+ if (litellm) {
194
+ const sorted = getSortedKeys(litellm);
195
+ for (const key of sorted) {
196
+ const keyLower = key.toLowerCase();
197
+ // Only accept if model is a superset of key (model contains key), to
198
+ // avoid e.g. "gpt-5" matching "gpt-5-pro" in the wrong direction.
199
+ if (lower.includes(keyLower) || (dotForm && dotForm.includes(keyLower))) {
200
+ return { hit: true, source: "litellm:fuzzy", value: litellm[key] };
201
+ }
202
+ }
203
+ }
204
+
205
+ return { hit: false, source: "miss", value: null };
206
+ }
207
+
208
+ // Convert one LiteLLM entry (per-token) to internal per-million USD shape.
209
+ // Missing fields stay missing — callers default with `(pricing.x || 0)`.
210
+ //
211
+ // Why the round: floating-point math means 1e-7 * 1e6 = 0.09999999999999999.
212
+ // Rounding to 10 significant decimals ($0.0000000001 / MTok) is well below
213
+ // any realistic price step but cleans up the printed/asserted numbers.
214
+ function roundToTenDecimals(n) {
215
+ return Math.round(n * 1e10) / 1e10;
216
+ }
217
+
218
+ function convertLitellmEntry(entry) {
219
+ if (!entry || typeof entry !== "object") return null;
220
+ const out = {};
221
+ if (typeof entry.input_cost_per_token === "number") {
222
+ out.input = roundToTenDecimals(entry.input_cost_per_token * 1_000_000);
223
+ }
224
+ if (typeof entry.output_cost_per_token === "number") {
225
+ out.output = roundToTenDecimals(entry.output_cost_per_token * 1_000_000);
226
+ }
227
+ if (typeof entry.cache_read_input_token_cost === "number") {
228
+ out.cache_read = roundToTenDecimals(entry.cache_read_input_token_cost * 1_000_000);
229
+ }
230
+ if (typeof entry.cache_creation_input_token_cost === "number") {
231
+ out.cache_write = roundToTenDecimals(entry.cache_creation_input_token_cost * 1_000_000);
232
+ }
233
+ return Object.keys(out).length ? out : null;
234
+ }
235
+
236
+ // Build a per-million-USD map from a LiteLLM raw map (or seed snapshot which
237
+ // uses the same field names). Skips meta keys starting with "_".
238
+ function buildLitellmPerMillionMap(rawData) {
239
+ if (!rawData || typeof rawData !== "object") return {};
240
+ const out = {};
241
+ for (const [name, entry] of Object.entries(rawData)) {
242
+ if (name.startsWith("_")) continue;
243
+ const converted = convertLitellmEntry(entry);
244
+ if (converted) out[name] = converted;
245
+ }
246
+ return out;
247
+ }
248
+
249
+ module.exports = {
250
+ lookupPricing,
251
+ stripReasoningSuffix,
252
+ normalizeAntigravityModel,
253
+ convertLitellmEntry,
254
+ buildLitellmPerMillionMap,
255
+ };