@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,341 @@
1
+ import type {
2
+ Insight,
3
+ InsightTemplate,
4
+ TemplateCondition,
5
+ Callsite,
6
+ EnrichedCallsite,
7
+ JoinedOutput,
8
+ PerformanceEnvelope,
9
+ } from './types.js';
10
+ import { getEnvelope, getThroughputPercent } from './envelopes.js';
11
+ import { getModelCost, calculateCost } from './costs.js';
12
+
13
+ // =============================================================================
14
+ // TYPES
15
+ // =============================================================================
16
+
17
+ interface EvaluationContext {
18
+ callsites?: Callsite[] | EnrichedCallsite[];
19
+ joined?: JoinedOutput;
20
+ envelopes?: Record<string, PerformanceEnvelope>;
21
+ }
22
+
23
+ interface GlobalStats {
24
+ totalCost: number;
25
+ costByCallsite: Map<string, number>;
26
+ top_callsite_cost_percent: number;
27
+ top_callsite_id: string;
28
+ top_callsite_model: string;
29
+ top_callsite_location: string;
30
+ }
31
+
32
+ // =============================================================================
33
+ // HELPERS
34
+ // =============================================================================
35
+
36
+ function getNestedValue(obj: unknown, path: string): unknown {
37
+ const parts = path.split('.');
38
+ let current: unknown = obj;
39
+
40
+ for (const part of parts) {
41
+ if (current === null || current === undefined) return undefined;
42
+ if (typeof current !== 'object') return undefined;
43
+ current = (current as Record<string, unknown>)[part];
44
+ }
45
+
46
+ return current;
47
+ }
48
+
49
+ function evaluateCondition(condition: TemplateCondition, context: Record<string, unknown>): boolean {
50
+ const fieldValue = getNestedValue(context, condition.field);
51
+
52
+ switch (condition.op) {
53
+ case 'eq':
54
+ return fieldValue === condition.value;
55
+
56
+ case 'neq':
57
+ return fieldValue !== condition.value;
58
+
59
+ case 'gt':
60
+ return typeof fieldValue === 'number' && typeof condition.value === 'number' && fieldValue > condition.value;
61
+
62
+ case 'lt':
63
+ return typeof fieldValue === 'number' && typeof condition.value === 'number' && fieldValue < condition.value;
64
+
65
+ case 'gte':
66
+ return typeof fieldValue === 'number' && typeof condition.value === 'number' && fieldValue >= condition.value;
67
+
68
+ case 'lte':
69
+ return typeof fieldValue === 'number' && typeof condition.value === 'number' && fieldValue <= condition.value;
70
+
71
+ case 'exists':
72
+ return fieldValue !== null && fieldValue !== undefined;
73
+
74
+ case 'in':
75
+ return Array.isArray(condition.value) && condition.value.includes(fieldValue as string);
76
+
77
+ case 'ratio_gt': {
78
+ if (!condition.compare_to || typeof condition.value !== 'number') return false;
79
+ const compareValue = getNestedValue(context, condition.compare_to);
80
+ if (typeof fieldValue !== 'number' || typeof compareValue !== 'number' || compareValue === 0) return false;
81
+ return (fieldValue / compareValue) > condition.value;
82
+ }
83
+
84
+ case 'ratio_lt': {
85
+ if (!condition.compare_to || typeof condition.value !== 'number') return false;
86
+ const compareValue = getNestedValue(context, condition.compare_to);
87
+ if (typeof fieldValue !== 'number' || typeof compareValue !== 'number' || compareValue === 0) return false;
88
+ return (fieldValue / compareValue) < condition.value;
89
+ }
90
+
91
+ case 'has_pattern': {
92
+ if (!condition.pattern || !Array.isArray(fieldValue)) return false;
93
+ const pattern = condition.pattern.toLowerCase();
94
+ const matchCount = (fieldValue as Callsite[]).filter(c =>
95
+ c.patterns.fallback === true ||
96
+ c.file.toLowerCase().includes(pattern)
97
+ ).length;
98
+ return condition.count_gt !== undefined ? matchCount > condition.count_gt : matchCount > 0;
99
+ }
100
+
101
+ default:
102
+ return false;
103
+ }
104
+ }
105
+
106
+ function interpolate(template: string, vars: Record<string, unknown>): string {
107
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
108
+ const value = vars[key];
109
+ if (value === undefined || value === null) return `{{${key}}}`;
110
+ if (typeof value === 'number') {
111
+ return Number.isInteger(value) ? value.toString() : value.toFixed(1);
112
+ }
113
+ return String(value);
114
+ });
115
+ }
116
+
117
+ function computeGlobalStats(callsites: EnrichedCallsite[]): GlobalStats {
118
+ const costByCallsite = new Map<string, number>();
119
+ let totalCost = 0;
120
+
121
+ for (const cs of callsites) {
122
+ if (cs.usage) {
123
+ const cost = calculateCost(cs.model || 'unknown', cs.usage.tokens_in, cs.usage.tokens_out);
124
+ costByCallsite.set(cs.id, cost);
125
+ totalCost += cost;
126
+ }
127
+ }
128
+
129
+ let topId = '';
130
+ let topCost = 0;
131
+ for (const [id, cost] of costByCallsite) {
132
+ if (cost > topCost) {
133
+ topCost = cost;
134
+ topId = id;
135
+ }
136
+ }
137
+
138
+ const topCallsite = callsites.find(c => c.id === topId);
139
+
140
+ return {
141
+ totalCost,
142
+ costByCallsite,
143
+ top_callsite_cost_percent: totalCost > 0 ? Math.round((topCost / totalCost) * 100) : 0,
144
+ top_callsite_id: topId,
145
+ top_callsite_model: topCallsite?.model || 'unknown',
146
+ top_callsite_location: topCallsite ? `${topCallsite.file}:${topCallsite.line}` : '',
147
+ };
148
+ }
149
+
150
+ // =============================================================================
151
+ // PUBLIC API
152
+ // =============================================================================
153
+
154
+ export function evaluate(
155
+ data: JoinedOutput | { callsites: Callsite[] | EnrichedCallsite[] },
156
+ templates: InsightTemplate[],
157
+ envelopes: Record<string, PerformanceEnvelope> = {}
158
+ ): Insight[] {
159
+ const insights: Insight[] = [];
160
+
161
+ const callsites = 'callsites' in data ? data.callsites : [];
162
+ const joined = 'codeOnly' in data ? data as JoinedOutput : null;
163
+ const globalStats = computeGlobalStats(callsites as EnrichedCallsite[]);
164
+
165
+ for (const template of templates) {
166
+ const { match, output, severity, category, id } = template;
167
+
168
+ switch (match.scope) {
169
+ case 'callsite': {
170
+ // Evaluate against each callsite
171
+ for (const callsite of callsites) {
172
+ const enriched = callsite as EnrichedCallsite;
173
+ const usage = enriched.usage;
174
+
175
+ // Build context with computed fields for condition evaluation
176
+ const context = {
177
+ ...callsite,
178
+ globalStats,
179
+ // Computed fields needed by templates
180
+ avg_tokens: usage ? Math.round(usage.tokens_out / usage.calls) : 0,
181
+ avg_tokens_in: usage ? Math.round(usage.tokens_in / usage.calls) : 0,
182
+ input_output_ratio: usage && usage.tokens_out > 0
183
+ ? Math.round(usage.tokens_in / usage.tokens_out)
184
+ : 0,
185
+ };
186
+ const allMatch = match.conditions.every(c => evaluateCondition(c, context));
187
+
188
+ if (allMatch) {
189
+ const vars: Record<string, unknown> = {
190
+ ...callsite,
191
+ location: `${callsite.file}:${callsite.line}`,
192
+ // Latency metrics
193
+ ratio: usage
194
+ ? (usage.latency_p99 / usage.latency_p50).toFixed(1)
195
+ : 'N/A',
196
+ p50: usage?.latency_p50,
197
+ p95: usage?.latency_p95,
198
+ p99: usage?.latency_p99,
199
+ // Token metrics
200
+ tokens_in: usage?.tokens_in || 0,
201
+ tokens_out: usage?.tokens_out || 0,
202
+ calls: usage?.calls || 0,
203
+ avg_tokens: usage
204
+ ? Math.round(usage.tokens_out / usage.calls)
205
+ : 0,
206
+ avg_tokens_in: usage
207
+ ? Math.round(usage.tokens_in / usage.calls)
208
+ : 0,
209
+ // Input/output ratio for prompt bloat detection
210
+ input_output_ratio: usage && usage.tokens_out > 0
211
+ ? Math.round(usage.tokens_in / usage.tokens_out)
212
+ : 0,
213
+ };
214
+
215
+ insights.push({
216
+ severity,
217
+ category,
218
+ templateId: id,
219
+ headline: interpolate(output.headline, vars),
220
+ evidence: interpolate(output.evidence, vars),
221
+ location: `${callsite.file}:${callsite.line}`,
222
+ source: 'template',
223
+ });
224
+ }
225
+ }
226
+ break;
227
+ }
228
+
229
+ case 'joined': {
230
+ if (!joined) continue;
231
+
232
+ const context = {
233
+ codeOnly: joined.codeOnly,
234
+ runtimeOnly: joined.runtimeOnly,
235
+ drift: joined.drift,
236
+ 'codeOnly.length': joined.codeOnly.length,
237
+ 'runtimeOnly.length': joined.runtimeOnly.length,
238
+ };
239
+
240
+ const allMatch = match.conditions.every(c => evaluateCondition(c, context));
241
+
242
+ if (allMatch) {
243
+ const vars: Record<string, unknown> = {
244
+ count: joined.codeOnly.length,
245
+ locations: joined.codeOnly
246
+ .slice(0, 3)
247
+ .map(c => `${c.file}:${c.line}`)
248
+ .join(', ') + (joined.codeOnly.length > 3 ? '...' : ''),
249
+ };
250
+
251
+ insights.push({
252
+ severity,
253
+ category,
254
+ templateId: id,
255
+ headline: interpolate(output.headline, vars),
256
+ evidence: interpolate(output.evidence, vars),
257
+ source: 'template',
258
+ });
259
+ }
260
+ break;
261
+ }
262
+
263
+ case 'global': {
264
+ const context = globalStats as unknown as Record<string, unknown>;
265
+ const allMatch = match.conditions.every(c => evaluateCondition(c, context));
266
+
267
+ if (allMatch) {
268
+ const vars: Record<string, unknown> = {
269
+ percent: globalStats.top_callsite_cost_percent,
270
+ model: globalStats.top_callsite_model,
271
+ location: globalStats.top_callsite_location,
272
+ };
273
+
274
+ insights.push({
275
+ severity,
276
+ category,
277
+ templateId: id,
278
+ headline: interpolate(output.headline, vars),
279
+ evidence: interpolate(output.evidence, vars),
280
+ location: globalStats.top_callsite_location,
281
+ source: 'template',
282
+ });
283
+ }
284
+ break;
285
+ }
286
+
287
+ case 'envelope': {
288
+ // Evaluate against each callsite with envelope comparison
289
+ for (const callsite of callsites) {
290
+ const enriched = callsite as EnrichedCallsite;
291
+ if (!enriched.usage || !callsite.model) continue;
292
+
293
+ const envelope = getEnvelope(callsite.model);
294
+ if (!envelope) continue;
295
+
296
+ // Calculate actual TPS (tokens per second)
297
+ const avgLatencySec = enriched.usage.latency_p50 / 1000;
298
+ const avgOutputTokens = enriched.usage.tokens_out / enriched.usage.calls;
299
+ const actualTps = avgLatencySec > 0 ? avgOutputTokens / avgLatencySec : 0;
300
+
301
+ const context = {
302
+ actual_tps: actualTps,
303
+ envelope,
304
+ 'envelope.tps_median': envelope.tps_median,
305
+ 'envelope.tps_peak': envelope.tps_peak,
306
+ };
307
+
308
+ const allMatch = match.conditions.every(c => evaluateCondition(c, context));
309
+
310
+ if (allMatch) {
311
+ const percent = getThroughputPercent(callsite.model, actualTps) || 0;
312
+ const vars: Record<string, unknown> = {
313
+ percent,
314
+ model: callsite.model,
315
+ actual: actualTps.toFixed(0),
316
+ reference: envelope.tps_median,
317
+ location: `${callsite.file}:${callsite.line}`,
318
+ };
319
+
320
+ insights.push({
321
+ severity,
322
+ category,
323
+ templateId: id,
324
+ headline: interpolate(output.headline, vars),
325
+ evidence: interpolate(output.evidence, vars),
326
+ location: `${callsite.file}:${callsite.line}`,
327
+ source: 'template',
328
+ });
329
+ }
330
+ }
331
+ break;
332
+ }
333
+ }
334
+ }
335
+
336
+ // Sort by severity: critical > warning > info
337
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
338
+ insights.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
339
+
340
+ return insights;
341
+ }
package/src/joiner.ts ADDED
@@ -0,0 +1,289 @@
1
+ import { percentile } from './runtime.js';
2
+ import type { Callsite, InferenceEvent, JoinedOutput, DriftSignal, EnrichedCallsite, UsageStats } from './types.js';
3
+
4
+ // =============================================================================
5
+ // CONFIGURABLE THRESHOLDS
6
+ // =============================================================================
7
+
8
+ export interface DriftThresholds {
9
+ /** Percentage threshold for streaming drift (0-100). Default: 50 */
10
+ streamingDriftPercent: number;
11
+ /** Minimum events before flagging retry/fallback drift. Default: 10 */
12
+ minEventsForPatternDrift: number;
13
+ }
14
+
15
+ const DEFAULT_THRESHOLDS: DriftThresholds = {
16
+ streamingDriftPercent: 50,
17
+ minEventsForPatternDrift: 10,
18
+ };
19
+
20
+ const currentThresholds = { ...DEFAULT_THRESHOLDS };
21
+
22
+ // =============================================================================
23
+ // HELPERS
24
+ // =============================================================================
25
+
26
+ function computeUsageStats(events: InferenceEvent[]): UsageStats {
27
+ const latencies = events.map(e => e.latency_ms);
28
+ return {
29
+ calls: events.length,
30
+ tokens_in: events.reduce((sum, e) => sum + e.input_tokens, 0),
31
+ tokens_out: events.reduce((sum, e) => sum + e.output_tokens, 0),
32
+ latency_p50: percentile(latencies, 50),
33
+ latency_p95: percentile(latencies, 95),
34
+ latency_p99: percentile(latencies, 99),
35
+ };
36
+ }
37
+
38
+ function makeKey(provider: string | null, model: string | null): string {
39
+ return `${provider || 'unknown'}:${model || 'unknown'}`;
40
+ }
41
+
42
+ /**
43
+ * Detect pattern drift between code-declared patterns and runtime behavior.
44
+ *
45
+ * Pattern types:
46
+ * - streaming: Code says stream=true but runtime shows non-streaming (or vice versa)
47
+ * - batching: Code declares batching but runtime shows no batch_id/batch_size
48
+ * - fallback: Code declares fallback but no fallback_used events seen
49
+ * - caching: Code declares caching but no cached events seen
50
+ * - retries: Code declares retries but no retry_count > 0 events seen
51
+ */
52
+ function detectPatternDrift(
53
+ callsite: Callsite,
54
+ events: InferenceEvent[]
55
+ ): DriftSignal[] {
56
+ const driftSignals: DriftSignal[] = [];
57
+ const patterns = callsite.patterns;
58
+
59
+ if (events.length === 0) {
60
+ return driftSignals;
61
+ }
62
+
63
+ // Count events with each pattern
64
+ const streamingEvents = events.filter(e => e.streaming === true).length;
65
+ const nonStreamingEvents = events.filter(e => e.streaming === false).length;
66
+ const batchedEvents = events.filter(e => e.batch_id || (e.batch_size && e.batch_size > 1)).length;
67
+ const cachedEvents = events.filter(e => e.cached === true).length;
68
+ const retriedEvents = events.filter(e => e.retry_count && e.retry_count > 0).length;
69
+ const fallbackEvents = events.filter(e => e.fallback_used === true).length;
70
+
71
+ // Calculate percentages
72
+ const streamingPercent = (streamingEvents / events.length) * 100;
73
+ const nonStreamingPercent = (nonStreamingEvents / events.length) * 100;
74
+
75
+ // 1. Streaming drift detection
76
+ if (patterns.streaming === true) {
77
+ // Code declares streaming, check if runtime confirms
78
+ if (nonStreamingPercent > currentThresholds.streamingDriftPercent) {
79
+ driftSignals.push({
80
+ type: 'patternDrift',
81
+ callsiteId: callsite.id,
82
+ provider: callsite.provider || undefined,
83
+ model: callsite.model || undefined,
84
+ message: `Streaming declared in code but ${nonStreamingPercent.toFixed(0)}% of runtime calls are non-streaming`,
85
+ });
86
+ } else if (streamingEvents === 0 && nonStreamingEvents === 0) {
87
+ // No streaming info available - soft warning
88
+ driftSignals.push({
89
+ type: 'patternDrift',
90
+ callsiteId: callsite.id,
91
+ provider: callsite.provider || undefined,
92
+ model: callsite.model || undefined,
93
+ message: `Streaming declared in code but cannot verify from runtime data (no TTFT metrics)`,
94
+ });
95
+ }
96
+ } else if (patterns.streaming === false) {
97
+ // Code explicitly non-streaming, but runtime shows streaming
98
+ if (streamingPercent > currentThresholds.streamingDriftPercent) {
99
+ driftSignals.push({
100
+ type: 'patternDrift',
101
+ callsiteId: callsite.id,
102
+ provider: callsite.provider || undefined,
103
+ model: callsite.model || undefined,
104
+ message: `Non-streaming declared in code but ${streamingPercent.toFixed(0)}% of runtime calls appear to stream`,
105
+ });
106
+ }
107
+ }
108
+
109
+ // 2. Batching drift detection
110
+ if (patterns.batching === true && batchedEvents === 0) {
111
+ driftSignals.push({
112
+ type: 'patternDrift',
113
+ callsiteId: callsite.id,
114
+ provider: callsite.provider || undefined,
115
+ model: callsite.model || undefined,
116
+ message: `Batching declared in code but no batched requests observed at runtime`,
117
+ });
118
+ }
119
+
120
+ // 3. Caching drift detection
121
+ if (patterns.caching === true && cachedEvents === 0) {
122
+ driftSignals.push({
123
+ type: 'patternDrift',
124
+ callsiteId: callsite.id,
125
+ provider: callsite.provider || undefined,
126
+ model: callsite.model || undefined,
127
+ message: `Caching declared in code but no cache hits observed at runtime`,
128
+ });
129
+ }
130
+
131
+ // 4. Retry drift detection
132
+ if (patterns.retries === true && retriedEvents === 0) {
133
+ // This could be good (no failures) or indicate retries aren't working
134
+ // Only flag if there are a significant number of events
135
+ if (events.length >= currentThresholds.minEventsForPatternDrift) {
136
+ driftSignals.push({
137
+ type: 'patternDrift',
138
+ callsiteId: callsite.id,
139
+ provider: callsite.provider || undefined,
140
+ model: callsite.model || undefined,
141
+ message: `Retries declared in code but no retry events observed (${events.length} calls, 0 retries)`,
142
+ });
143
+ }
144
+ }
145
+
146
+ // 5. Fallback drift detection
147
+ if (patterns.fallback === true && fallbackEvents === 0) {
148
+ // This could be good (primary always works) or indicate fallback isn't working
149
+ // Only flag if there are a significant number of events
150
+ if (events.length >= currentThresholds.minEventsForPatternDrift) {
151
+ driftSignals.push({
152
+ type: 'patternDrift',
153
+ callsiteId: callsite.id,
154
+ provider: callsite.provider || undefined,
155
+ model: callsite.model || undefined,
156
+ message: `Fallback declared in code but never triggered at runtime (${events.length} calls)`,
157
+ });
158
+ }
159
+ }
160
+
161
+ return driftSignals;
162
+ }
163
+
164
+ // =============================================================================
165
+ // PUBLIC API
166
+ // =============================================================================
167
+
168
+ export function join(callsites: Callsite[], events: InferenceEvent[]): JoinedOutput {
169
+ const codeOnly: Callsite[] = [];
170
+ const runtimeOnly: InferenceEvent[] = [];
171
+ const drift: DriftSignal[] = [];
172
+ const enrichedCallsites: EnrichedCallsite[] = [];
173
+
174
+ // Separate events: those with callsite_id vs those without
175
+ const eventsByCallsiteId = new Map<string, InferenceEvent[]>();
176
+ const eventsWithoutCallsiteId: InferenceEvent[] = [];
177
+
178
+ for (const event of events) {
179
+ if (event.callsite_id) {
180
+ // Events with callsite_id ONLY match by callsite_id, not by provider+model
181
+ if (!eventsByCallsiteId.has(event.callsite_id)) {
182
+ eventsByCallsiteId.set(event.callsite_id, []);
183
+ }
184
+ eventsByCallsiteId.get(event.callsite_id)!.push(event);
185
+ } else {
186
+ // Events without callsite_id will be matched by provider+model
187
+ eventsWithoutCallsiteId.push(event);
188
+ }
189
+ }
190
+
191
+ // Group events without callsite_id by provider+model
192
+ const eventsByKey = new Map<string, InferenceEvent[]>();
193
+ for (const event of eventsWithoutCallsiteId) {
194
+ const key = makeKey(event.provider, event.model);
195
+ if (!eventsByKey.has(key)) {
196
+ eventsByKey.set(key, []);
197
+ }
198
+ eventsByKey.get(key)!.push(event);
199
+ }
200
+
201
+ // Track which keys have been matched
202
+ const matchedKeys = new Set<string>();
203
+ const matchedCallsiteIds = new Set<string>();
204
+
205
+ // Match callsites to events
206
+ for (const callsite of callsites) {
207
+ let matchedEvents: InferenceEvent[] = [];
208
+
209
+ // Priority 1: Match by callsite_id (events that explicitly reference this callsite)
210
+ if (eventsByCallsiteId.has(callsite.id)) {
211
+ matchedEvents = eventsByCallsiteId.get(callsite.id)!;
212
+ matchedCallsiteIds.add(callsite.id);
213
+ }
214
+
215
+ // Priority 2: Match by provider+model (only for events without callsite_id)
216
+ if (matchedEvents.length === 0) {
217
+ const key = makeKey(callsite.provider, callsite.model);
218
+ const keyEvents = eventsByKey.get(key);
219
+ if (keyEvents && keyEvents.length > 0) {
220
+ matchedEvents = keyEvents;
221
+ matchedKeys.add(key);
222
+ }
223
+ }
224
+
225
+ // Build enriched callsite
226
+ if (matchedEvents.length > 0) {
227
+ const usage = computeUsageStats(matchedEvents);
228
+ enrichedCallsites.push({ ...callsite, usage });
229
+
230
+ // Detect pattern drift for matched callsites
231
+ const patternDrift = detectPatternDrift(callsite, matchedEvents);
232
+ drift.push(...patternDrift);
233
+ } else {
234
+ // No matching events - this is code-only
235
+ enrichedCallsites.push(callsite);
236
+ codeOnly.push(callsite);
237
+
238
+ drift.push({
239
+ type: 'codeOnly',
240
+ provider: callsite.provider || undefined,
241
+ model: callsite.model || undefined,
242
+ callsiteId: callsite.id,
243
+ message: `Callsite ${callsite.file}:${callsite.line} has no runtime events`,
244
+ });
245
+ }
246
+ }
247
+
248
+ // Find runtime-only events
249
+ // 1. Events with callsite_id that don't match any callsite
250
+ for (const [callsiteId, evts] of eventsByCallsiteId) {
251
+ if (!matchedCallsiteIds.has(callsiteId)) {
252
+ runtimeOnly.push(...evts);
253
+ }
254
+ }
255
+
256
+ // 2. Events without callsite_id whose key wasn't matched
257
+ for (const [key, evts] of eventsByKey) {
258
+ if (!matchedKeys.has(key)) {
259
+ runtimeOnly.push(...evts);
260
+ }
261
+ }
262
+
263
+ // Generate drift signals for runtime-only events (grouped by provider+model)
264
+ const runtimeOnlyByKey = new Map<string, InferenceEvent[]>();
265
+ for (const event of runtimeOnly) {
266
+ const key = makeKey(event.provider, event.model);
267
+ if (!runtimeOnlyByKey.has(key)) {
268
+ runtimeOnlyByKey.set(key, []);
269
+ }
270
+ runtimeOnlyByKey.get(key)!.push(event);
271
+ }
272
+
273
+ for (const [key, evts] of runtimeOnlyByKey) {
274
+ const [provider, model] = key.split(':');
275
+ drift.push({
276
+ type: 'runtimeOnly',
277
+ provider: provider || undefined,
278
+ model: model || undefined,
279
+ message: `${evts.length} events for ${key} with no matching code`,
280
+ });
281
+ }
282
+
283
+ return {
284
+ callsites: enrichedCallsites,
285
+ codeOnly,
286
+ runtimeOnly,
287
+ drift,
288
+ };
289
+ }