@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.
- package/.claude/settings.local.json +8 -0
- package/.env.example +6 -0
- package/.github/workflows/peakinfer.yml +64 -0
- package/CHANGELOG.md +31 -0
- package/LICENSE +190 -0
- package/README.md +335 -0
- package/data/inferencemax.json +274 -0
- package/dist/agent-analyzer.d.ts +45 -0
- package/dist/agent-analyzer.d.ts.map +1 -0
- package/dist/agent-analyzer.js +374 -0
- package/dist/agent-analyzer.js.map +1 -0
- package/dist/agent.d.ts +76 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +965 -0
- package/dist/agent.js.map +1 -0
- package/dist/agents/correlation-analyzer.d.ts +34 -0
- package/dist/agents/correlation-analyzer.d.ts.map +1 -0
- package/dist/agents/correlation-analyzer.js +261 -0
- package/dist/agents/correlation-analyzer.js.map +1 -0
- package/dist/agents/index.d.ts +91 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +111 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/runtime-analyzer.d.ts +38 -0
- package/dist/agents/runtime-analyzer.d.ts.map +1 -0
- package/dist/agents/runtime-analyzer.js +244 -0
- package/dist/agents/runtime-analyzer.js.map +1 -0
- package/dist/analysis-types.d.ts +500 -0
- package/dist/analysis-types.d.ts.map +1 -0
- package/dist/analysis-types.js +11 -0
- package/dist/analysis-types.js.map +1 -0
- package/dist/analytics.d.ts +25 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +94 -0
- package/dist/analytics.js.map +1 -0
- package/dist/analyzer.d.ts +48 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +547 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/artifacts.d.ts +44 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +165 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/benchmarks/index.d.ts +88 -0
- package/dist/benchmarks/index.d.ts.map +1 -0
- package/dist/benchmarks/index.js +205 -0
- package/dist/benchmarks/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +427 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/ci.d.ts +19 -0
- package/dist/commands/ci.d.ts.map +1 -0
- package/dist/commands/ci.js +253 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +249 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/demo.d.ts +15 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +106 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/export.d.ts +14 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +209 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/history.d.ts +15 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +389 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/template.d.ts +14 -0
- package/dist/commands/template.d.ts.map +1 -0
- package/dist/commands/template.js +341 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/validate-map.d.ts +12 -0
- package/dist/commands/validate-map.d.ts.map +1 -0
- package/dist/commands/validate-map.js +274 -0
- package/dist/commands/validate-map.js.map +1 -0
- package/dist/commands/whatif.d.ts +17 -0
- package/dist/commands/whatif.d.ts.map +1 -0
- package/dist/commands/whatif.js +206 -0
- package/dist/commands/whatif.js.map +1 -0
- package/dist/comparison.d.ts +38 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +223 -0
- package/dist/comparison.js.map +1 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +158 -0
- package/dist/config.js.map +1 -0
- package/dist/connectors/helicone.d.ts +9 -0
- package/dist/connectors/helicone.d.ts.map +1 -0
- package/dist/connectors/helicone.js +106 -0
- package/dist/connectors/helicone.js.map +1 -0
- package/dist/connectors/index.d.ts +37 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +65 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/langsmith.d.ts +9 -0
- package/dist/connectors/langsmith.d.ts.map +1 -0
- package/dist/connectors/langsmith.js +122 -0
- package/dist/connectors/langsmith.js.map +1 -0
- package/dist/connectors/types.d.ts +83 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +98 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/cost-estimator.d.ts +46 -0
- package/dist/cost-estimator.d.ts.map +1 -0
- package/dist/cost-estimator.js +104 -0
- package/dist/cost-estimator.js.map +1 -0
- package/dist/costs.d.ts +57 -0
- package/dist/costs.d.ts.map +1 -0
- package/dist/costs.js +251 -0
- package/dist/costs.js.map +1 -0
- package/dist/counterfactuals.d.ts +29 -0
- package/dist/counterfactuals.d.ts.map +1 -0
- package/dist/counterfactuals.js +448 -0
- package/dist/counterfactuals.js.map +1 -0
- package/dist/enhancement-prompts.d.ts +41 -0
- package/dist/enhancement-prompts.d.ts.map +1 -0
- package/dist/enhancement-prompts.js +88 -0
- package/dist/enhancement-prompts.js.map +1 -0
- package/dist/envelopes.d.ts +20 -0
- package/dist/envelopes.d.ts.map +1 -0
- package/dist/envelopes.js +790 -0
- package/dist/envelopes.js.map +1 -0
- package/dist/format-normalizer.d.ts +71 -0
- package/dist/format-normalizer.d.ts.map +1 -0
- package/dist/format-normalizer.js +1331 -0
- package/dist/format-normalizer.js.map +1 -0
- package/dist/history.d.ts +79 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +313 -0
- package/dist/history.js.map +1 -0
- package/dist/html.d.ts +11 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +463 -0
- package/dist/html.js.map +1 -0
- package/dist/impact.d.ts +42 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +443 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/insights.d.ts +5 -0
- package/dist/insights.d.ts.map +1 -0
- package/dist/insights.js +271 -0
- package/dist/insights.js.map +1 -0
- package/dist/joiner.d.ts +9 -0
- package/dist/joiner.d.ts.map +1 -0
- package/dist/joiner.js +247 -0
- package/dist/joiner.js.map +1 -0
- package/dist/orchestrator.d.ts +34 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +827 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/pdf.d.ts +26 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +84 -0
- package/dist/pdf.js.map +1 -0
- package/dist/prediction.d.ts +33 -0
- package/dist/prediction.d.ts.map +1 -0
- package/dist/prediction.js +316 -0
- package/dist/prediction.js.map +1 -0
- package/dist/prompts/loader.d.ts +38 -0
- package/dist/prompts/loader.d.ts.map +1 -0
- package/dist/prompts/loader.js +60 -0
- package/dist/prompts/loader.js.map +1 -0
- package/dist/renderer.d.ts +64 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +923 -0
- package/dist/renderer.js.map +1 -0
- package/dist/runid.d.ts +57 -0
- package/dist/runid.d.ts.map +1 -0
- package/dist/runid.js +199 -0
- package/dist/runid.js.map +1 -0
- package/dist/runtime.d.ts +29 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +366 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scanner.d.ts +11 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +426 -0
- package/dist/scanner.js.map +1 -0
- package/dist/templates.d.ts +120 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +429 -0
- package/dist/templates.js.map +1 -0
- package/dist/tools/index.d.ts +153 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +177 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types.d.ts +3647 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +703 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/docs/demo-guide.md +423 -0
- package/docs/events-format.md +295 -0
- package/docs/inferencemap-spec.md +344 -0
- package/docs/migration-v2.md +293 -0
- package/fixtures/demo/precomputed.json +142 -0
- package/fixtures/demo-project/README.md +52 -0
- package/fixtures/demo-project/ai-service.ts +65 -0
- package/fixtures/demo-project/sample-events.jsonl +15 -0
- package/fixtures/demo-project/src/ai-service.ts +128 -0
- package/fixtures/demo-project/src/llm-client.ts +155 -0
- package/package.json +65 -0
- package/prompts/agent-analyzer.yaml +47 -0
- package/prompts/ci-gate.yaml +98 -0
- package/prompts/correlation-analyzer.yaml +178 -0
- package/prompts/format-normalizer.yaml +46 -0
- package/prompts/peak-performance.yaml +180 -0
- package/prompts/pr-comment.yaml +111 -0
- package/prompts/runtime-analyzer.yaml +189 -0
- package/prompts/unified-analyzer.yaml +241 -0
- package/schemas/inference-map.v0.1.json +215 -0
- package/scripts/benchmark.ts +394 -0
- package/scripts/demo-v1.5.sh +158 -0
- package/scripts/sync-from-site.sh +197 -0
- package/scripts/validate-sync.sh +178 -0
- package/src/agent-analyzer.ts +481 -0
- package/src/agent.ts +1232 -0
- package/src/agents/correlation-analyzer.ts +353 -0
- package/src/agents/index.ts +235 -0
- package/src/agents/runtime-analyzer.ts +343 -0
- package/src/analysis-types.ts +558 -0
- package/src/analytics.ts +100 -0
- package/src/analyzer.ts +692 -0
- package/src/artifacts.ts +218 -0
- package/src/benchmarks/index.ts +309 -0
- package/src/cli.ts +503 -0
- package/src/commands/ci.ts +336 -0
- package/src/commands/config.ts +288 -0
- package/src/commands/demo.ts +175 -0
- package/src/commands/export.ts +297 -0
- package/src/commands/history.ts +425 -0
- package/src/commands/template.ts +385 -0
- package/src/commands/validate-map.ts +324 -0
- package/src/commands/whatif.ts +272 -0
- package/src/comparison.ts +283 -0
- package/src/config.ts +188 -0
- package/src/connectors/helicone.ts +164 -0
- package/src/connectors/index.ts +93 -0
- package/src/connectors/langsmith.ts +179 -0
- package/src/connectors/types.ts +180 -0
- package/src/cost-estimator.ts +146 -0
- package/src/costs.ts +347 -0
- package/src/counterfactuals.ts +516 -0
- package/src/enhancement-prompts.ts +118 -0
- package/src/envelopes.ts +814 -0
- package/src/format-normalizer.ts +1486 -0
- package/src/history.ts +400 -0
- package/src/html.ts +512 -0
- package/src/impact.ts +522 -0
- package/src/index.ts +83 -0
- package/src/insights.ts +341 -0
- package/src/joiner.ts +289 -0
- package/src/orchestrator.ts +1015 -0
- package/src/pdf.ts +110 -0
- package/src/prediction.ts +392 -0
- package/src/prompts/loader.ts +88 -0
- package/src/renderer.ts +1045 -0
- package/src/runid.ts +261 -0
- package/src/runtime.ts +450 -0
- package/src/scanner.ts +508 -0
- package/src/templates.ts +561 -0
- package/src/tools/index.ts +214 -0
- package/src/types.ts +873 -0
- package/src/version.ts +24 -0
- package/templates/context-accumulation.yaml +23 -0
- package/templates/cost-concentration.yaml +20 -0
- package/templates/dead-code.yaml +20 -0
- package/templates/latency-explainer.yaml +23 -0
- package/templates/optimizations/ab-testing-framework.yaml +74 -0
- package/templates/optimizations/api-gateway-optimization.yaml +81 -0
- package/templates/optimizations/api-model-routing-strategy.yaml +126 -0
- package/templates/optimizations/auto-scaling-optimization.yaml +85 -0
- package/templates/optimizations/batch-utilization-diagnostic.yaml +142 -0
- package/templates/optimizations/comprehensive-apm.yaml +76 -0
- package/templates/optimizations/context-window-optimization.yaml +91 -0
- package/templates/optimizations/cost-sensitive-batch-processing.yaml +77 -0
- package/templates/optimizations/distributed-training-optimization.yaml +77 -0
- package/templates/optimizations/document-analysis-edge.yaml +77 -0
- package/templates/optimizations/document-pipeline-optimization.yaml +78 -0
- package/templates/optimizations/domain-specific-distillation.yaml +78 -0
- package/templates/optimizations/error-handling-optimization.yaml +76 -0
- package/templates/optimizations/gptq-4bit-quantization.yaml +96 -0
- package/templates/optimizations/long-context-memory-management.yaml +78 -0
- package/templates/optimizations/max-tokens-optimization.yaml +76 -0
- package/templates/optimizations/memory-bandwidth-optimization.yaml +73 -0
- package/templates/optimizations/multi-framework-resilience.yaml +75 -0
- package/templates/optimizations/multi-tenant-optimization.yaml +75 -0
- package/templates/optimizations/prompt-caching-optimization.yaml +143 -0
- package/templates/optimizations/pytorch-to-onnx-migration.yaml +109 -0
- package/templates/optimizations/quality-monitoring.yaml +74 -0
- package/templates/optimizations/realtime-budget-controls.yaml +74 -0
- package/templates/optimizations/realtime-latency-optimization.yaml +74 -0
- package/templates/optimizations/sglang-concurrency-optimization.yaml +78 -0
- package/templates/optimizations/smart-model-routing.yaml +96 -0
- package/templates/optimizations/streaming-batch-selection.yaml +167 -0
- package/templates/optimizations/system-prompt-optimization.yaml +75 -0
- package/templates/optimizations/tensorrt-llm-performance.yaml +77 -0
- package/templates/optimizations/vllm-high-throughput-optimization.yaml +93 -0
- package/templates/optimizations/vllm-migration-memory-bound.yaml +78 -0
- package/templates/overpowered-extraction.yaml +32 -0
- package/templates/overpowered-model.yaml +31 -0
- package/templates/prompt-bloat.yaml +24 -0
- package/templates/retry-explosion.yaml +28 -0
- package/templates/schema/insight.schema.json +113 -0
- package/templates/schema/optimization.schema.json +180 -0
- package/templates/streaming-drift.yaml +30 -0
- package/templates/throughput-gap.yaml +21 -0
- package/templates/token-underutilization.yaml +28 -0
- package/templates/untested-fallback.yaml +21 -0
- package/tests/accuracy/drift-detection.test.ts +184 -0
- package/tests/accuracy/false-positives.test.ts +166 -0
- package/tests/accuracy/templates.test.ts +205 -0
- package/tests/action/commands.test.ts +125 -0
- package/tests/action/comments.test.ts +347 -0
- package/tests/cli.test.ts +203 -0
- package/tests/comparison.test.ts +309 -0
- package/tests/correlation-analyzer.test.ts +534 -0
- package/tests/counterfactuals.test.ts +347 -0
- package/tests/fixtures/events/missing-id.jsonl +1 -0
- package/tests/fixtures/events/missing-input.jsonl +1 -0
- package/tests/fixtures/events/missing-latency.jsonl +1 -0
- package/tests/fixtures/events/missing-model.jsonl +1 -0
- package/tests/fixtures/events/missing-output.jsonl +1 -0
- package/tests/fixtures/events/missing-provider.jsonl +1 -0
- package/tests/fixtures/events/missing-ts.jsonl +1 -0
- package/tests/fixtures/events/valid.csv +3 -0
- package/tests/fixtures/events/valid.json +1 -0
- package/tests/fixtures/events/valid.jsonl +2 -0
- package/tests/fixtures/events/with-callsite.jsonl +1 -0
- package/tests/fixtures/events/with-intent.jsonl +1 -0
- package/tests/fixtures/events/wrong-type.jsonl +1 -0
- package/tests/fixtures/repos/empty/.gitkeep +0 -0
- package/tests/fixtures/repos/hybrid-router/router.py +35 -0
- package/tests/fixtures/repos/saas-anthropic/agent.ts +27 -0
- package/tests/fixtures/repos/saas-openai/assistant.js +33 -0
- package/tests/fixtures/repos/saas-openai/client.py +26 -0
- package/tests/fixtures/repos/self-hosted-vllm/inference.py +22 -0
- package/tests/github-action.test.ts +292 -0
- package/tests/insights.test.ts +878 -0
- package/tests/joiner.test.ts +168 -0
- package/tests/performance/action-latency.test.ts +132 -0
- package/tests/performance/benchmark.test.ts +189 -0
- package/tests/performance/cli-latency.test.ts +102 -0
- package/tests/pr-comment.test.ts +313 -0
- package/tests/prediction.test.ts +296 -0
- package/tests/runtime-analyzer.test.ts +375 -0
- package/tests/runtime.test.ts +205 -0
- package/tests/scanner.test.ts +122 -0
- package/tests/template-conformance.test.ts +526 -0
- package/tests/unit/cost-calculator.test.ts +303 -0
- package/tests/unit/credits.test.ts +180 -0
- package/tests/unit/inference-map.test.ts +276 -0
- package/tests/unit/schema.test.ts +300 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +14 -0
package/src/insights.ts
ADDED
|
@@ -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
|
+
}
|