@peakinfer/cli 1.0.133

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 (367) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.env.example +6 -0
  3. package/.github/workflows/peakinfer.yml +64 -0
  4. package/CHANGELOG.md +31 -0
  5. package/LICENSE +190 -0
  6. package/README.md +335 -0
  7. package/data/inferencemax.json +274 -0
  8. package/dist/agent-analyzer.d.ts +45 -0
  9. package/dist/agent-analyzer.d.ts.map +1 -0
  10. package/dist/agent-analyzer.js +374 -0
  11. package/dist/agent-analyzer.js.map +1 -0
  12. package/dist/agent.d.ts +76 -0
  13. package/dist/agent.d.ts.map +1 -0
  14. package/dist/agent.js +965 -0
  15. package/dist/agent.js.map +1 -0
  16. package/dist/agents/correlation-analyzer.d.ts +34 -0
  17. package/dist/agents/correlation-analyzer.d.ts.map +1 -0
  18. package/dist/agents/correlation-analyzer.js +261 -0
  19. package/dist/agents/correlation-analyzer.js.map +1 -0
  20. package/dist/agents/index.d.ts +91 -0
  21. package/dist/agents/index.d.ts.map +1 -0
  22. package/dist/agents/index.js +111 -0
  23. package/dist/agents/index.js.map +1 -0
  24. package/dist/agents/runtime-analyzer.d.ts +38 -0
  25. package/dist/agents/runtime-analyzer.d.ts.map +1 -0
  26. package/dist/agents/runtime-analyzer.js +244 -0
  27. package/dist/agents/runtime-analyzer.js.map +1 -0
  28. package/dist/analysis-types.d.ts +500 -0
  29. package/dist/analysis-types.d.ts.map +1 -0
  30. package/dist/analysis-types.js +11 -0
  31. package/dist/analysis-types.js.map +1 -0
  32. package/dist/analytics.d.ts +25 -0
  33. package/dist/analytics.d.ts.map +1 -0
  34. package/dist/analytics.js +94 -0
  35. package/dist/analytics.js.map +1 -0
  36. package/dist/analyzer.d.ts +48 -0
  37. package/dist/analyzer.d.ts.map +1 -0
  38. package/dist/analyzer.js +547 -0
  39. package/dist/analyzer.js.map +1 -0
  40. package/dist/artifacts.d.ts +44 -0
  41. package/dist/artifacts.d.ts.map +1 -0
  42. package/dist/artifacts.js +165 -0
  43. package/dist/artifacts.js.map +1 -0
  44. package/dist/benchmarks/index.d.ts +88 -0
  45. package/dist/benchmarks/index.d.ts.map +1 -0
  46. package/dist/benchmarks/index.js +205 -0
  47. package/dist/benchmarks/index.js.map +1 -0
  48. package/dist/cli.d.ts +3 -0
  49. package/dist/cli.d.ts.map +1 -0
  50. package/dist/cli.js +427 -0
  51. package/dist/cli.js.map +1 -0
  52. package/dist/commands/ci.d.ts +19 -0
  53. package/dist/commands/ci.d.ts.map +1 -0
  54. package/dist/commands/ci.js +253 -0
  55. package/dist/commands/ci.js.map +1 -0
  56. package/dist/commands/config.d.ts +16 -0
  57. package/dist/commands/config.d.ts.map +1 -0
  58. package/dist/commands/config.js +249 -0
  59. package/dist/commands/config.js.map +1 -0
  60. package/dist/commands/demo.d.ts +15 -0
  61. package/dist/commands/demo.d.ts.map +1 -0
  62. package/dist/commands/demo.js +106 -0
  63. package/dist/commands/demo.js.map +1 -0
  64. package/dist/commands/export.d.ts +14 -0
  65. package/dist/commands/export.d.ts.map +1 -0
  66. package/dist/commands/export.js +209 -0
  67. package/dist/commands/export.js.map +1 -0
  68. package/dist/commands/history.d.ts +15 -0
  69. package/dist/commands/history.d.ts.map +1 -0
  70. package/dist/commands/history.js +389 -0
  71. package/dist/commands/history.js.map +1 -0
  72. package/dist/commands/template.d.ts +14 -0
  73. package/dist/commands/template.d.ts.map +1 -0
  74. package/dist/commands/template.js +341 -0
  75. package/dist/commands/template.js.map +1 -0
  76. package/dist/commands/validate-map.d.ts +12 -0
  77. package/dist/commands/validate-map.d.ts.map +1 -0
  78. package/dist/commands/validate-map.js +274 -0
  79. package/dist/commands/validate-map.js.map +1 -0
  80. package/dist/commands/whatif.d.ts +17 -0
  81. package/dist/commands/whatif.d.ts.map +1 -0
  82. package/dist/commands/whatif.js +206 -0
  83. package/dist/commands/whatif.js.map +1 -0
  84. package/dist/comparison.d.ts +38 -0
  85. package/dist/comparison.d.ts.map +1 -0
  86. package/dist/comparison.js +223 -0
  87. package/dist/comparison.js.map +1 -0
  88. package/dist/config.d.ts +42 -0
  89. package/dist/config.d.ts.map +1 -0
  90. package/dist/config.js +158 -0
  91. package/dist/config.js.map +1 -0
  92. package/dist/connectors/helicone.d.ts +9 -0
  93. package/dist/connectors/helicone.d.ts.map +1 -0
  94. package/dist/connectors/helicone.js +106 -0
  95. package/dist/connectors/helicone.js.map +1 -0
  96. package/dist/connectors/index.d.ts +37 -0
  97. package/dist/connectors/index.d.ts.map +1 -0
  98. package/dist/connectors/index.js +65 -0
  99. package/dist/connectors/index.js.map +1 -0
  100. package/dist/connectors/langsmith.d.ts +9 -0
  101. package/dist/connectors/langsmith.d.ts.map +1 -0
  102. package/dist/connectors/langsmith.js +122 -0
  103. package/dist/connectors/langsmith.js.map +1 -0
  104. package/dist/connectors/types.d.ts +83 -0
  105. package/dist/connectors/types.d.ts.map +1 -0
  106. package/dist/connectors/types.js +98 -0
  107. package/dist/connectors/types.js.map +1 -0
  108. package/dist/cost-estimator.d.ts +46 -0
  109. package/dist/cost-estimator.d.ts.map +1 -0
  110. package/dist/cost-estimator.js +104 -0
  111. package/dist/cost-estimator.js.map +1 -0
  112. package/dist/costs.d.ts +57 -0
  113. package/dist/costs.d.ts.map +1 -0
  114. package/dist/costs.js +251 -0
  115. package/dist/costs.js.map +1 -0
  116. package/dist/counterfactuals.d.ts +29 -0
  117. package/dist/counterfactuals.d.ts.map +1 -0
  118. package/dist/counterfactuals.js +448 -0
  119. package/dist/counterfactuals.js.map +1 -0
  120. package/dist/enhancement-prompts.d.ts +41 -0
  121. package/dist/enhancement-prompts.d.ts.map +1 -0
  122. package/dist/enhancement-prompts.js +88 -0
  123. package/dist/enhancement-prompts.js.map +1 -0
  124. package/dist/envelopes.d.ts +20 -0
  125. package/dist/envelopes.d.ts.map +1 -0
  126. package/dist/envelopes.js +790 -0
  127. package/dist/envelopes.js.map +1 -0
  128. package/dist/format-normalizer.d.ts +71 -0
  129. package/dist/format-normalizer.d.ts.map +1 -0
  130. package/dist/format-normalizer.js +1331 -0
  131. package/dist/format-normalizer.js.map +1 -0
  132. package/dist/history.d.ts +79 -0
  133. package/dist/history.d.ts.map +1 -0
  134. package/dist/history.js +313 -0
  135. package/dist/history.js.map +1 -0
  136. package/dist/html.d.ts +11 -0
  137. package/dist/html.d.ts.map +1 -0
  138. package/dist/html.js +463 -0
  139. package/dist/html.js.map +1 -0
  140. package/dist/impact.d.ts +42 -0
  141. package/dist/impact.d.ts.map +1 -0
  142. package/dist/impact.js +443 -0
  143. package/dist/impact.js.map +1 -0
  144. package/dist/index.d.ts +26 -0
  145. package/dist/index.d.ts.map +1 -0
  146. package/dist/index.js +34 -0
  147. package/dist/index.js.map +1 -0
  148. package/dist/insights.d.ts +5 -0
  149. package/dist/insights.d.ts.map +1 -0
  150. package/dist/insights.js +271 -0
  151. package/dist/insights.js.map +1 -0
  152. package/dist/joiner.d.ts +9 -0
  153. package/dist/joiner.d.ts.map +1 -0
  154. package/dist/joiner.js +247 -0
  155. package/dist/joiner.js.map +1 -0
  156. package/dist/orchestrator.d.ts +34 -0
  157. package/dist/orchestrator.d.ts.map +1 -0
  158. package/dist/orchestrator.js +827 -0
  159. package/dist/orchestrator.js.map +1 -0
  160. package/dist/pdf.d.ts +26 -0
  161. package/dist/pdf.d.ts.map +1 -0
  162. package/dist/pdf.js +84 -0
  163. package/dist/pdf.js.map +1 -0
  164. package/dist/prediction.d.ts +33 -0
  165. package/dist/prediction.d.ts.map +1 -0
  166. package/dist/prediction.js +316 -0
  167. package/dist/prediction.js.map +1 -0
  168. package/dist/prompts/loader.d.ts +38 -0
  169. package/dist/prompts/loader.d.ts.map +1 -0
  170. package/dist/prompts/loader.js +60 -0
  171. package/dist/prompts/loader.js.map +1 -0
  172. package/dist/renderer.d.ts +64 -0
  173. package/dist/renderer.d.ts.map +1 -0
  174. package/dist/renderer.js +923 -0
  175. package/dist/renderer.js.map +1 -0
  176. package/dist/runid.d.ts +57 -0
  177. package/dist/runid.d.ts.map +1 -0
  178. package/dist/runid.js +199 -0
  179. package/dist/runid.js.map +1 -0
  180. package/dist/runtime.d.ts +29 -0
  181. package/dist/runtime.d.ts.map +1 -0
  182. package/dist/runtime.js +366 -0
  183. package/dist/runtime.js.map +1 -0
  184. package/dist/scanner.d.ts +11 -0
  185. package/dist/scanner.d.ts.map +1 -0
  186. package/dist/scanner.js +426 -0
  187. package/dist/scanner.js.map +1 -0
  188. package/dist/templates.d.ts +120 -0
  189. package/dist/templates.d.ts.map +1 -0
  190. package/dist/templates.js +429 -0
  191. package/dist/templates.js.map +1 -0
  192. package/dist/tools/index.d.ts +153 -0
  193. package/dist/tools/index.d.ts.map +1 -0
  194. package/dist/tools/index.js +177 -0
  195. package/dist/tools/index.js.map +1 -0
  196. package/dist/types.d.ts +3647 -0
  197. package/dist/types.d.ts.map +1 -0
  198. package/dist/types.js +703 -0
  199. package/dist/types.js.map +1 -0
  200. package/dist/version.d.ts +7 -0
  201. package/dist/version.d.ts.map +1 -0
  202. package/dist/version.js +23 -0
  203. package/dist/version.js.map +1 -0
  204. package/docs/demo-guide.md +423 -0
  205. package/docs/events-format.md +295 -0
  206. package/docs/inferencemap-spec.md +344 -0
  207. package/docs/migration-v2.md +293 -0
  208. package/fixtures/demo/precomputed.json +142 -0
  209. package/fixtures/demo-project/README.md +52 -0
  210. package/fixtures/demo-project/ai-service.ts +65 -0
  211. package/fixtures/demo-project/sample-events.jsonl +15 -0
  212. package/fixtures/demo-project/src/ai-service.ts +128 -0
  213. package/fixtures/demo-project/src/llm-client.ts +155 -0
  214. package/package.json +65 -0
  215. package/prompts/agent-analyzer.yaml +47 -0
  216. package/prompts/ci-gate.yaml +98 -0
  217. package/prompts/correlation-analyzer.yaml +178 -0
  218. package/prompts/format-normalizer.yaml +46 -0
  219. package/prompts/peak-performance.yaml +180 -0
  220. package/prompts/pr-comment.yaml +111 -0
  221. package/prompts/runtime-analyzer.yaml +189 -0
  222. package/prompts/unified-analyzer.yaml +241 -0
  223. package/schemas/inference-map.v0.1.json +215 -0
  224. package/scripts/benchmark.ts +394 -0
  225. package/scripts/demo-v1.5.sh +158 -0
  226. package/scripts/sync-from-site.sh +197 -0
  227. package/scripts/validate-sync.sh +178 -0
  228. package/src/agent-analyzer.ts +481 -0
  229. package/src/agent.ts +1232 -0
  230. package/src/agents/correlation-analyzer.ts +353 -0
  231. package/src/agents/index.ts +235 -0
  232. package/src/agents/runtime-analyzer.ts +343 -0
  233. package/src/analysis-types.ts +558 -0
  234. package/src/analytics.ts +100 -0
  235. package/src/analyzer.ts +692 -0
  236. package/src/artifacts.ts +218 -0
  237. package/src/benchmarks/index.ts +309 -0
  238. package/src/cli.ts +503 -0
  239. package/src/commands/ci.ts +336 -0
  240. package/src/commands/config.ts +288 -0
  241. package/src/commands/demo.ts +175 -0
  242. package/src/commands/export.ts +297 -0
  243. package/src/commands/history.ts +425 -0
  244. package/src/commands/template.ts +385 -0
  245. package/src/commands/validate-map.ts +324 -0
  246. package/src/commands/whatif.ts +272 -0
  247. package/src/comparison.ts +283 -0
  248. package/src/config.ts +188 -0
  249. package/src/connectors/helicone.ts +164 -0
  250. package/src/connectors/index.ts +93 -0
  251. package/src/connectors/langsmith.ts +179 -0
  252. package/src/connectors/types.ts +180 -0
  253. package/src/cost-estimator.ts +146 -0
  254. package/src/costs.ts +347 -0
  255. package/src/counterfactuals.ts +516 -0
  256. package/src/enhancement-prompts.ts +118 -0
  257. package/src/envelopes.ts +814 -0
  258. package/src/format-normalizer.ts +1486 -0
  259. package/src/history.ts +400 -0
  260. package/src/html.ts +512 -0
  261. package/src/impact.ts +522 -0
  262. package/src/index.ts +83 -0
  263. package/src/insights.ts +341 -0
  264. package/src/joiner.ts +289 -0
  265. package/src/orchestrator.ts +1015 -0
  266. package/src/pdf.ts +110 -0
  267. package/src/prediction.ts +392 -0
  268. package/src/prompts/loader.ts +88 -0
  269. package/src/renderer.ts +1045 -0
  270. package/src/runid.ts +261 -0
  271. package/src/runtime.ts +450 -0
  272. package/src/scanner.ts +508 -0
  273. package/src/templates.ts +561 -0
  274. package/src/tools/index.ts +214 -0
  275. package/src/types.ts +873 -0
  276. package/src/version.ts +24 -0
  277. package/templates/context-accumulation.yaml +23 -0
  278. package/templates/cost-concentration.yaml +20 -0
  279. package/templates/dead-code.yaml +20 -0
  280. package/templates/latency-explainer.yaml +23 -0
  281. package/templates/optimizations/ab-testing-framework.yaml +74 -0
  282. package/templates/optimizations/api-gateway-optimization.yaml +81 -0
  283. package/templates/optimizations/api-model-routing-strategy.yaml +126 -0
  284. package/templates/optimizations/auto-scaling-optimization.yaml +85 -0
  285. package/templates/optimizations/batch-utilization-diagnostic.yaml +142 -0
  286. package/templates/optimizations/comprehensive-apm.yaml +76 -0
  287. package/templates/optimizations/context-window-optimization.yaml +91 -0
  288. package/templates/optimizations/cost-sensitive-batch-processing.yaml +77 -0
  289. package/templates/optimizations/distributed-training-optimization.yaml +77 -0
  290. package/templates/optimizations/document-analysis-edge.yaml +77 -0
  291. package/templates/optimizations/document-pipeline-optimization.yaml +78 -0
  292. package/templates/optimizations/domain-specific-distillation.yaml +78 -0
  293. package/templates/optimizations/error-handling-optimization.yaml +76 -0
  294. package/templates/optimizations/gptq-4bit-quantization.yaml +96 -0
  295. package/templates/optimizations/long-context-memory-management.yaml +78 -0
  296. package/templates/optimizations/max-tokens-optimization.yaml +76 -0
  297. package/templates/optimizations/memory-bandwidth-optimization.yaml +73 -0
  298. package/templates/optimizations/multi-framework-resilience.yaml +75 -0
  299. package/templates/optimizations/multi-tenant-optimization.yaml +75 -0
  300. package/templates/optimizations/prompt-caching-optimization.yaml +143 -0
  301. package/templates/optimizations/pytorch-to-onnx-migration.yaml +109 -0
  302. package/templates/optimizations/quality-monitoring.yaml +74 -0
  303. package/templates/optimizations/realtime-budget-controls.yaml +74 -0
  304. package/templates/optimizations/realtime-latency-optimization.yaml +74 -0
  305. package/templates/optimizations/sglang-concurrency-optimization.yaml +78 -0
  306. package/templates/optimizations/smart-model-routing.yaml +96 -0
  307. package/templates/optimizations/streaming-batch-selection.yaml +167 -0
  308. package/templates/optimizations/system-prompt-optimization.yaml +75 -0
  309. package/templates/optimizations/tensorrt-llm-performance.yaml +77 -0
  310. package/templates/optimizations/vllm-high-throughput-optimization.yaml +93 -0
  311. package/templates/optimizations/vllm-migration-memory-bound.yaml +78 -0
  312. package/templates/overpowered-extraction.yaml +32 -0
  313. package/templates/overpowered-model.yaml +31 -0
  314. package/templates/prompt-bloat.yaml +24 -0
  315. package/templates/retry-explosion.yaml +28 -0
  316. package/templates/schema/insight.schema.json +113 -0
  317. package/templates/schema/optimization.schema.json +180 -0
  318. package/templates/streaming-drift.yaml +30 -0
  319. package/templates/throughput-gap.yaml +21 -0
  320. package/templates/token-underutilization.yaml +28 -0
  321. package/templates/untested-fallback.yaml +21 -0
  322. package/tests/accuracy/drift-detection.test.ts +184 -0
  323. package/tests/accuracy/false-positives.test.ts +166 -0
  324. package/tests/accuracy/templates.test.ts +205 -0
  325. package/tests/action/commands.test.ts +125 -0
  326. package/tests/action/comments.test.ts +347 -0
  327. package/tests/cli.test.ts +203 -0
  328. package/tests/comparison.test.ts +309 -0
  329. package/tests/correlation-analyzer.test.ts +534 -0
  330. package/tests/counterfactuals.test.ts +347 -0
  331. package/tests/fixtures/events/missing-id.jsonl +1 -0
  332. package/tests/fixtures/events/missing-input.jsonl +1 -0
  333. package/tests/fixtures/events/missing-latency.jsonl +1 -0
  334. package/tests/fixtures/events/missing-model.jsonl +1 -0
  335. package/tests/fixtures/events/missing-output.jsonl +1 -0
  336. package/tests/fixtures/events/missing-provider.jsonl +1 -0
  337. package/tests/fixtures/events/missing-ts.jsonl +1 -0
  338. package/tests/fixtures/events/valid.csv +3 -0
  339. package/tests/fixtures/events/valid.json +1 -0
  340. package/tests/fixtures/events/valid.jsonl +2 -0
  341. package/tests/fixtures/events/with-callsite.jsonl +1 -0
  342. package/tests/fixtures/events/with-intent.jsonl +1 -0
  343. package/tests/fixtures/events/wrong-type.jsonl +1 -0
  344. package/tests/fixtures/repos/empty/.gitkeep +0 -0
  345. package/tests/fixtures/repos/hybrid-router/router.py +35 -0
  346. package/tests/fixtures/repos/saas-anthropic/agent.ts +27 -0
  347. package/tests/fixtures/repos/saas-openai/assistant.js +33 -0
  348. package/tests/fixtures/repos/saas-openai/client.py +26 -0
  349. package/tests/fixtures/repos/self-hosted-vllm/inference.py +22 -0
  350. package/tests/github-action.test.ts +292 -0
  351. package/tests/insights.test.ts +878 -0
  352. package/tests/joiner.test.ts +168 -0
  353. package/tests/performance/action-latency.test.ts +132 -0
  354. package/tests/performance/benchmark.test.ts +189 -0
  355. package/tests/performance/cli-latency.test.ts +102 -0
  356. package/tests/pr-comment.test.ts +313 -0
  357. package/tests/prediction.test.ts +296 -0
  358. package/tests/runtime-analyzer.test.ts +375 -0
  359. package/tests/runtime.test.ts +205 -0
  360. package/tests/scanner.test.ts +122 -0
  361. package/tests/template-conformance.test.ts +526 -0
  362. package/tests/unit/cost-calculator.test.ts +303 -0
  363. package/tests/unit/credits.test.ts +180 -0
  364. package/tests/unit/inference-map.test.ts +276 -0
  365. package/tests/unit/schema.test.ts +300 -0
  366. package/tsconfig.json +20 -0
  367. package/vitest.config.ts +14 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Cost Calculator Tests
3
+ * Per Test Cases v1.9.3 - Core Analysis Tests
4
+ */
5
+ import { describe, test, expect } from 'vitest';
6
+
7
+ // Types for cost calculation
8
+ interface ModelPricing {
9
+ provider: string;
10
+ model: string;
11
+ inputPer1M: number; // Cost per 1M input tokens
12
+ outputPer1M: number; // Cost per 1M output tokens
13
+ cachedInputPer1M?: number; // Cost for cached input tokens
14
+ }
15
+
16
+ interface TokenUsage {
17
+ inputTokens: number;
18
+ outputTokens: number;
19
+ cachedTokens?: number;
20
+ }
21
+
22
+ interface CostEstimate {
23
+ inputCost: number;
24
+ outputCost: number;
25
+ cachedCost: number;
26
+ totalCost: number;
27
+ }
28
+
29
+ // Model pricing database (subset from LiteLLM pricing)
30
+ const MODEL_PRICING: ModelPricing[] = [
31
+ // Anthropic
32
+ { provider: 'anthropic', model: 'claude-opus-4-20250514', inputPer1M: 15, outputPer1M: 75, cachedInputPer1M: 1.5 },
33
+ { provider: 'anthropic', model: 'claude-sonnet-4-20250514', inputPer1M: 3, outputPer1M: 15, cachedInputPer1M: 0.3 },
34
+ { provider: 'anthropic', model: 'claude-3-5-sonnet-20241022', inputPer1M: 3, outputPer1M: 15, cachedInputPer1M: 0.3 },
35
+ { provider: 'anthropic', model: 'claude-haiku-3-20240307', inputPer1M: 0.25, outputPer1M: 1.25, cachedInputPer1M: 0.03 },
36
+ // OpenAI
37
+ { provider: 'openai', model: 'gpt-4o', inputPer1M: 2.5, outputPer1M: 10, cachedInputPer1M: 1.25 },
38
+ { provider: 'openai', model: 'gpt-4o-mini', inputPer1M: 0.15, outputPer1M: 0.6, cachedInputPer1M: 0.075 },
39
+ { provider: 'openai', model: 'gpt-4-turbo', inputPer1M: 10, outputPer1M: 30 },
40
+ { provider: 'openai', model: 'o1', inputPer1M: 15, outputPer1M: 60, cachedInputPer1M: 7.5 },
41
+ { provider: 'openai', model: 'o1-mini', inputPer1M: 1.1, outputPer1M: 4.4, cachedInputPer1M: 0.55 },
42
+ // Embeddings
43
+ { provider: 'openai', model: 'text-embedding-3-small', inputPer1M: 0.02, outputPer1M: 0 },
44
+ { provider: 'openai', model: 'text-embedding-3-large', inputPer1M: 0.13, outputPer1M: 0 },
45
+ ];
46
+
47
+ // Cost calculation functions
48
+ function getPricing(provider: string, model: string): ModelPricing | null {
49
+ // Try exact match first
50
+ let pricing = MODEL_PRICING.find(p => p.provider === provider && p.model === model);
51
+ if (pricing) return pricing;
52
+
53
+ // Try partial match (model name contains)
54
+ pricing = MODEL_PRICING.find(p => p.provider === provider && model.includes(p.model));
55
+ if (pricing) return pricing;
56
+
57
+ // Try partial match (pricing model contains model name)
58
+ pricing = MODEL_PRICING.find(p => p.provider === provider && p.model.includes(model));
59
+ return pricing || null;
60
+ }
61
+
62
+ function calculateCost(pricing: ModelPricing, usage: TokenUsage): CostEstimate {
63
+ const inputCost = (usage.inputTokens / 1_000_000) * pricing.inputPer1M;
64
+ const outputCost = (usage.outputTokens / 1_000_000) * pricing.outputPer1M;
65
+ const cachedCost = usage.cachedTokens && pricing.cachedInputPer1M
66
+ ? (usage.cachedTokens / 1_000_000) * pricing.cachedInputPer1M
67
+ : 0;
68
+
69
+ return {
70
+ inputCost,
71
+ outputCost,
72
+ cachedCost,
73
+ totalCost: inputCost + outputCost + cachedCost,
74
+ };
75
+ }
76
+
77
+ function estimateMonthlyCost(
78
+ pricing: ModelPricing,
79
+ usage: TokenUsage,
80
+ requestsPerDay: number
81
+ ): number {
82
+ const perRequest = calculateCost(pricing, usage).totalCost;
83
+ return perRequest * requestsPerDay * 30;
84
+ }
85
+
86
+ function compareCosts(
87
+ usage: TokenUsage,
88
+ model1: { provider: string; model: string },
89
+ model2: { provider: string; model: string }
90
+ ): { savings: number; percentSaved: number } | null {
91
+ const pricing1 = getPricing(model1.provider, model1.model);
92
+ const pricing2 = getPricing(model2.provider, model2.model);
93
+
94
+ if (!pricing1 || !pricing2) return null;
95
+
96
+ const cost1 = calculateCost(pricing1, usage).totalCost;
97
+ const cost2 = calculateCost(pricing2, usage).totalCost;
98
+
99
+ const savings = cost1 - cost2;
100
+ const percentSaved = cost1 > 0 ? (savings / cost1) * 100 : 0;
101
+
102
+ return { savings, percentSaved };
103
+ }
104
+
105
+ describe('Model Pricing Lookup', () => {
106
+ test('Finds exact model match', () => {
107
+ const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514');
108
+ expect(pricing).not.toBeNull();
109
+ expect(pricing?.inputPer1M).toBe(3);
110
+ expect(pricing?.outputPer1M).toBe(15);
111
+ });
112
+
113
+ test('Returns null for unknown model', () => {
114
+ const pricing = getPricing('anthropic', 'unknown-model');
115
+ expect(pricing).toBeNull();
116
+ });
117
+
118
+ test('Returns null for unknown provider', () => {
119
+ const pricing = getPricing('unknown-provider', 'gpt-4');
120
+ expect(pricing).toBeNull();
121
+ });
122
+
123
+ test('Finds OpenAI models', () => {
124
+ const pricing = getPricing('openai', 'gpt-4o');
125
+ expect(pricing).not.toBeNull();
126
+ expect(pricing?.inputPer1M).toBe(2.5);
127
+ });
128
+
129
+ test('Finds embedding models', () => {
130
+ const pricing = getPricing('openai', 'text-embedding-3-small');
131
+ expect(pricing).not.toBeNull();
132
+ expect(pricing?.outputPer1M).toBe(0); // Embeddings have no output cost
133
+ });
134
+ });
135
+
136
+ describe('Cost Calculation', () => {
137
+ test('Calculates cost for typical usage', () => {
138
+ const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
139
+ const usage: TokenUsage = {
140
+ inputTokens: 1000,
141
+ outputTokens: 500,
142
+ };
143
+
144
+ const cost = calculateCost(pricing, usage);
145
+
146
+ // 1000 input tokens at $3/1M = $0.003
147
+ expect(cost.inputCost).toBeCloseTo(0.003, 6);
148
+ // 500 output tokens at $15/1M = $0.0075
149
+ expect(cost.outputCost).toBeCloseTo(0.0075, 6);
150
+ expect(cost.totalCost).toBeCloseTo(0.0105, 6);
151
+ });
152
+
153
+ test('Calculates cost with cached tokens', () => {
154
+ const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
155
+ const usage: TokenUsage = {
156
+ inputTokens: 500,
157
+ outputTokens: 500,
158
+ cachedTokens: 500,
159
+ };
160
+
161
+ const cost = calculateCost(pricing, usage);
162
+
163
+ // 500 input tokens at $3/1M = $0.0015
164
+ expect(cost.inputCost).toBeCloseTo(0.0015, 6);
165
+ // 500 cached tokens at $0.30/1M = $0.00015
166
+ expect(cost.cachedCost).toBeCloseTo(0.00015, 6);
167
+ expect(cost.totalCost).toBeCloseTo(0.0015 + 0.0075 + 0.00015, 6);
168
+ });
169
+
170
+ test('Handles zero tokens', () => {
171
+ const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
172
+ const usage: TokenUsage = {
173
+ inputTokens: 0,
174
+ outputTokens: 0,
175
+ };
176
+
177
+ const cost = calculateCost(pricing, usage);
178
+
179
+ expect(cost.totalCost).toBe(0);
180
+ });
181
+
182
+ test('Handles large token counts', () => {
183
+ const pricing = getPricing('openai', 'gpt-4o')!;
184
+ const usage: TokenUsage = {
185
+ inputTokens: 1_000_000,
186
+ outputTokens: 500_000,
187
+ };
188
+
189
+ const cost = calculateCost(pricing, usage);
190
+
191
+ // 1M input at $2.5/1M = $2.5
192
+ expect(cost.inputCost).toBeCloseTo(2.5, 2);
193
+ // 500K output at $10/1M = $5
194
+ expect(cost.outputCost).toBeCloseTo(5, 2);
195
+ expect(cost.totalCost).toBeCloseTo(7.5, 2);
196
+ });
197
+ });
198
+
199
+ describe('Monthly Cost Estimation', () => {
200
+ test('Estimates monthly cost correctly', () => {
201
+ const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
202
+ const usage: TokenUsage = {
203
+ inputTokens: 2000,
204
+ outputTokens: 1000,
205
+ };
206
+
207
+ const monthlyCost = estimateMonthlyCost(pricing, usage, 100); // 100 requests/day
208
+
209
+ // Per request: (2000 * 3 + 1000 * 15) / 1M = 0.021
210
+ // Monthly: 0.021 * 100 * 30 = 63
211
+ expect(monthlyCost).toBeCloseTo(63, 0);
212
+ });
213
+
214
+ test('Handles zero requests', () => {
215
+ const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
216
+ const usage: TokenUsage = { inputTokens: 1000, outputTokens: 1000 };
217
+
218
+ const monthlyCost = estimateMonthlyCost(pricing, usage, 0);
219
+
220
+ expect(monthlyCost).toBe(0);
221
+ });
222
+ });
223
+
224
+ describe('Cost Comparison', () => {
225
+ test('Compares costs between models', () => {
226
+ const usage: TokenUsage = {
227
+ inputTokens: 10000,
228
+ outputTokens: 5000,
229
+ };
230
+
231
+ const comparison = compareCosts(
232
+ usage,
233
+ { provider: 'anthropic', model: 'claude-opus-4-20250514' },
234
+ { provider: 'anthropic', model: 'claude-sonnet-4-20250514' }
235
+ );
236
+
237
+ expect(comparison).not.toBeNull();
238
+ expect(comparison!.savings).toBeGreaterThan(0); // Opus is more expensive
239
+ expect(comparison!.percentSaved).toBeGreaterThan(0);
240
+ });
241
+
242
+ test('Shows negative savings when upgrading to expensive model', () => {
243
+ const usage: TokenUsage = {
244
+ inputTokens: 10000,
245
+ outputTokens: 5000,
246
+ };
247
+
248
+ const comparison = compareCosts(
249
+ usage,
250
+ { provider: 'anthropic', model: 'claude-sonnet-4-20250514' },
251
+ { provider: 'anthropic', model: 'claude-opus-4-20250514' }
252
+ );
253
+
254
+ expect(comparison).not.toBeNull();
255
+ expect(comparison!.savings).toBeLessThan(0); // Sonnet is cheaper
256
+ });
257
+
258
+ test('Returns null for unknown models', () => {
259
+ const usage: TokenUsage = { inputTokens: 1000, outputTokens: 1000 };
260
+
261
+ const comparison = compareCosts(
262
+ usage,
263
+ { provider: 'anthropic', model: 'unknown' },
264
+ { provider: 'anthropic', model: 'claude-sonnet-4-20250514' }
265
+ );
266
+
267
+ expect(comparison).toBeNull();
268
+ });
269
+
270
+ test('Calculates significant savings for haiku vs opus', () => {
271
+ const usage: TokenUsage = {
272
+ inputTokens: 100000,
273
+ outputTokens: 50000,
274
+ };
275
+
276
+ const comparison = compareCosts(
277
+ usage,
278
+ { provider: 'anthropic', model: 'claude-opus-4-20250514' },
279
+ { provider: 'anthropic', model: 'claude-haiku-3-20240307' }
280
+ );
281
+
282
+ expect(comparison).not.toBeNull();
283
+ // Haiku should be ~60x cheaper for input, ~60x cheaper for output
284
+ expect(comparison!.percentSaved).toBeGreaterThan(90);
285
+ });
286
+ });
287
+
288
+ describe('Embedding Costs', () => {
289
+ test('Calculates embedding costs (output is zero)', () => {
290
+ const pricing = getPricing('openai', 'text-embedding-3-small')!;
291
+ const usage: TokenUsage = {
292
+ inputTokens: 8000, // Typical embedding request
293
+ outputTokens: 0,
294
+ };
295
+
296
+ const cost = calculateCost(pricing, usage);
297
+
298
+ // 8000 tokens at $0.02/1M = $0.00016
299
+ expect(cost.inputCost).toBeCloseTo(0.00016, 6);
300
+ expect(cost.outputCost).toBe(0);
301
+ expect(cost.totalCost).toBeCloseTo(0.00016, 6);
302
+ });
303
+ });
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Credit System Tests
3
+ * Per Test Cases v1.9.3 - Critical Path Tests
4
+ */
5
+ import { describe, test, expect, beforeEach } from 'vitest';
6
+
7
+ // Types
8
+ interface CreditPack {
9
+ credits: number;
10
+ purchasedAt: string;
11
+ expired?: boolean;
12
+ }
13
+
14
+ interface AnalysisOptions {
15
+ static?: boolean;
16
+ runtime?: boolean;
17
+ templates?: boolean;
18
+ historical?: boolean;
19
+ }
20
+
21
+ // Functions under test (to be implemented in src/credits.ts)
22
+ function calculateCost(options: AnalysisOptions): number {
23
+ let cost = 0;
24
+ if (options.static) cost += 1;
25
+ if (options.runtime) cost += 1;
26
+ if (options.templates) cost += 1;
27
+ if (options.historical) cost += 1;
28
+ return cost;
29
+ }
30
+
31
+ function consumeCredits(packs: CreditPack[], amount: number): { consumed: number; remaining: number } {
32
+ let remaining = amount;
33
+
34
+ // Sort by purchase date (FIFO)
35
+ const sortedPacks = [...packs].sort(
36
+ (a, b) => new Date(a.purchasedAt).getTime() - new Date(b.purchasedAt).getTime()
37
+ );
38
+
39
+ for (const pack of sortedPacks) {
40
+ if (pack.expired) continue;
41
+
42
+ const toConsume = Math.min(pack.credits, remaining);
43
+ pack.credits -= toConsume;
44
+ remaining -= toConsume;
45
+
46
+ if (remaining === 0) break;
47
+ }
48
+
49
+ if (remaining > 0) {
50
+ throw new Error('Insufficient credits');
51
+ }
52
+
53
+ return { consumed: amount, remaining: 0 };
54
+ }
55
+
56
+ function isExpired(purchasedAt: string, expiryMonths: number = 6): boolean {
57
+ const purchaseDate = new Date(purchasedAt);
58
+ const expiryDate = new Date(purchaseDate);
59
+ expiryDate.setMonth(expiryDate.getMonth() + expiryMonths);
60
+ return new Date() > expiryDate;
61
+ }
62
+
63
+ function getTotalCredits(packs: CreditPack[]): number {
64
+ return packs
65
+ .filter(pack => !pack.expired)
66
+ .reduce((sum, pack) => sum + pack.credits, 0);
67
+ }
68
+
69
+ // Tests
70
+ describe('Credit Calculation', () => {
71
+ test('Static-only analysis costs 1 credit', () => {
72
+ expect(calculateCost({ static: true, runtime: false })).toBe(1);
73
+ });
74
+
75
+ test('Runtime analysis costs 2 credits', () => {
76
+ expect(calculateCost({ static: true, runtime: true })).toBe(2);
77
+ });
78
+
79
+ test('Template suggestions cost 3 credits', () => {
80
+ expect(calculateCost({ static: true, runtime: true, templates: true })).toBe(3);
81
+ });
82
+
83
+ test('Historical comparison adds 1 credit', () => {
84
+ expect(calculateCost({ static: true, historical: true })).toBe(2);
85
+ });
86
+
87
+ test('Full analysis costs 4 credits', () => {
88
+ expect(calculateCost({ static: true, runtime: true, templates: true, historical: true })).toBe(4);
89
+ });
90
+
91
+ test('Empty options costs 0 credits', () => {
92
+ expect(calculateCost({})).toBe(0);
93
+ });
94
+ });
95
+
96
+ describe('FIFO Consumption', () => {
97
+ test('Oldest credits consumed first', () => {
98
+ const packs: CreditPack[] = [
99
+ { credits: 50, purchasedAt: '2025-01-01' },
100
+ { credits: 200, purchasedAt: '2025-06-01' },
101
+ ];
102
+ consumeCredits(packs, 30);
103
+ expect(packs[0].credits).toBe(20); // Oldest reduced
104
+ expect(packs[1].credits).toBe(200); // Newer untouched
105
+ });
106
+
107
+ test('Expired credits skipped', () => {
108
+ const packs: CreditPack[] = [
109
+ { credits: 50, purchasedAt: '2024-01-01', expired: true },
110
+ { credits: 200, purchasedAt: '2025-06-01' },
111
+ ];
112
+ consumeCredits(packs, 30);
113
+ expect(packs[0].credits).toBe(50); // Expired untouched
114
+ expect(packs[1].credits).toBe(170); // Active reduced
115
+ });
116
+
117
+ test('Cannot go negative', () => {
118
+ const packs: CreditPack[] = [{ credits: 10, purchasedAt: '2025-01-01' }];
119
+ expect(() => consumeCredits(packs, 20)).toThrow('Insufficient credits');
120
+ });
121
+
122
+ test('Consumes across multiple packs', () => {
123
+ const packs: CreditPack[] = [
124
+ { credits: 30, purchasedAt: '2025-01-01' },
125
+ { credits: 50, purchasedAt: '2025-02-01' },
126
+ ];
127
+ consumeCredits(packs, 50);
128
+ expect(packs[0].credits).toBe(0);
129
+ expect(packs[1].credits).toBe(30);
130
+ });
131
+
132
+ test('Exact consumption works', () => {
133
+ const packs: CreditPack[] = [{ credits: 50, purchasedAt: '2025-01-01' }];
134
+ consumeCredits(packs, 50);
135
+ expect(packs[0].credits).toBe(0);
136
+ });
137
+ });
138
+
139
+ describe('Credit Expiration', () => {
140
+ test('Credits expire after 6 months', () => {
141
+ const oldDate = new Date();
142
+ oldDate.setMonth(oldDate.getMonth() - 7);
143
+ expect(isExpired(oldDate.toISOString())).toBe(true);
144
+ });
145
+
146
+ test('Recent credits are not expired', () => {
147
+ const recentDate = new Date();
148
+ recentDate.setMonth(recentDate.getMonth() - 1);
149
+ expect(isExpired(recentDate.toISOString())).toBe(false);
150
+ });
151
+
152
+ test('Credits at exactly 6 months are not expired', () => {
153
+ const exactDate = new Date();
154
+ exactDate.setMonth(exactDate.getMonth() - 6);
155
+ exactDate.setDate(exactDate.getDate() + 1); // Just under 6 months
156
+ expect(isExpired(exactDate.toISOString())).toBe(false);
157
+ });
158
+ });
159
+
160
+ describe('Total Credits', () => {
161
+ test('Sums all non-expired packs', () => {
162
+ const packs: CreditPack[] = [
163
+ { credits: 50, purchasedAt: '2025-01-01' },
164
+ { credits: 200, purchasedAt: '2025-06-01' },
165
+ ];
166
+ expect(getTotalCredits(packs)).toBe(250);
167
+ });
168
+
169
+ test('Excludes expired packs', () => {
170
+ const packs: CreditPack[] = [
171
+ { credits: 50, purchasedAt: '2025-01-01', expired: true },
172
+ { credits: 200, purchasedAt: '2025-06-01' },
173
+ ];
174
+ expect(getTotalCredits(packs)).toBe(200);
175
+ });
176
+
177
+ test('Empty packs returns 0', () => {
178
+ expect(getTotalCredits([])).toBe(0);
179
+ });
180
+ });