@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/runid.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import type { InferenceMap, Insight, JoinedOutput, RuntimeSummary } from './types.js';
|
|
5
|
+
import { VERSION } from './version.js';
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// TYPES
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
export interface RunInputs {
|
|
12
|
+
repoRoot?: string;
|
|
13
|
+
eventsPath?: string;
|
|
14
|
+
offline?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RunManifest {
|
|
18
|
+
runId: string;
|
|
19
|
+
version: string;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
inputs: {
|
|
22
|
+
repoRoot?: string;
|
|
23
|
+
repoHash?: string;
|
|
24
|
+
eventsPath?: string;
|
|
25
|
+
eventsHash?: string;
|
|
26
|
+
offline: boolean;
|
|
27
|
+
};
|
|
28
|
+
artifacts: string[];
|
|
29
|
+
status: 'complete' | 'partial' | 'failed';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CachedArtifacts {
|
|
33
|
+
inferenceMap?: InferenceMap;
|
|
34
|
+
insights?: Insight[];
|
|
35
|
+
joined?: JoinedOutput;
|
|
36
|
+
runtime?: RuntimeSummary;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// HELPERS
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hash a file's content for change detection
|
|
45
|
+
*/
|
|
46
|
+
function hashFile(filePath: string): string {
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
49
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
50
|
+
} catch {
|
|
51
|
+
return 'missing';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hash a directory structure (file paths + sizes + mtimes)
|
|
57
|
+
* This is fast and detects most changes without reading all file contents
|
|
58
|
+
*/
|
|
59
|
+
function hashDirectory(dirPath: string, maxDepth: number = 5): string {
|
|
60
|
+
const hash = createHash('sha256');
|
|
61
|
+
|
|
62
|
+
function walk(dir: string, depth: number): void {
|
|
63
|
+
if (depth > maxDepth) return;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
67
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const fullPath = join(dir, entry.name);
|
|
71
|
+
|
|
72
|
+
// Skip common non-code directories
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
if (['node_modules', 'dist', '.git', '__pycache__', '.peakinfer', '.venv', 'venv'].includes(entry.name)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
walk(fullPath, depth + 1);
|
|
78
|
+
} else {
|
|
79
|
+
// Only hash source files
|
|
80
|
+
if (/\.(ts|tsx|js|jsx|py|go|java|rs|rb|php|cs)$/.test(entry.name)) {
|
|
81
|
+
try {
|
|
82
|
+
const stat = statSync(fullPath);
|
|
83
|
+
hash.update(`${fullPath}:${stat.size}:${stat.mtimeMs}`);
|
|
84
|
+
} catch {
|
|
85
|
+
// Skip files we can't stat
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Skip directories we can't read
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
walk(dirPath, 0);
|
|
96
|
+
return hash.digest('hex').slice(0, 16);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// PUBLIC API
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate a deterministic run ID based on inputs
|
|
105
|
+
*
|
|
106
|
+
* runId = hash(version, repoHash?, eventsHash?, offline)
|
|
107
|
+
*
|
|
108
|
+
* This ensures:
|
|
109
|
+
* - Same inputs = same runId = can resume
|
|
110
|
+
* - Changed inputs = new runId = fresh analysis
|
|
111
|
+
*/
|
|
112
|
+
export function generateRunId(inputs: RunInputs): string {
|
|
113
|
+
const hash = createHash('sha256');
|
|
114
|
+
|
|
115
|
+
// Version ensures cache invalidation on tool updates
|
|
116
|
+
hash.update(`v:${VERSION}`);
|
|
117
|
+
|
|
118
|
+
// Hash repo structure if provided
|
|
119
|
+
if (inputs.repoRoot && existsSync(inputs.repoRoot)) {
|
|
120
|
+
const repoHash = hashDirectory(inputs.repoRoot);
|
|
121
|
+
hash.update(`repo:${repoHash}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Hash events file if provided
|
|
125
|
+
if (inputs.eventsPath && existsSync(inputs.eventsPath)) {
|
|
126
|
+
const eventsHash = hashFile(inputs.eventsPath);
|
|
127
|
+
hash.update(`events:${eventsHash}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Offline mode affects template loading
|
|
131
|
+
hash.update(`offline:${inputs.offline ?? false}`);
|
|
132
|
+
|
|
133
|
+
return hash.digest('hex').slice(0, 12);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get the run directory path
|
|
138
|
+
*/
|
|
139
|
+
export function getRunDir(baseDir: string, runId: string): string {
|
|
140
|
+
return join(baseDir, 'runs', runId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create run manifest
|
|
145
|
+
*/
|
|
146
|
+
export function createManifest(
|
|
147
|
+
runId: string,
|
|
148
|
+
inputs: RunInputs,
|
|
149
|
+
artifacts: string[],
|
|
150
|
+
status: 'complete' | 'partial' | 'failed'
|
|
151
|
+
): RunManifest {
|
|
152
|
+
return {
|
|
153
|
+
runId,
|
|
154
|
+
version: VERSION,
|
|
155
|
+
createdAt: new Date().toISOString(),
|
|
156
|
+
inputs: {
|
|
157
|
+
repoRoot: inputs.repoRoot,
|
|
158
|
+
repoHash: inputs.repoRoot && existsSync(inputs.repoRoot)
|
|
159
|
+
? hashDirectory(inputs.repoRoot)
|
|
160
|
+
: undefined,
|
|
161
|
+
eventsPath: inputs.eventsPath,
|
|
162
|
+
eventsHash: inputs.eventsPath && existsSync(inputs.eventsPath)
|
|
163
|
+
? hashFile(inputs.eventsPath)
|
|
164
|
+
: undefined,
|
|
165
|
+
offline: inputs.offline ?? false,
|
|
166
|
+
},
|
|
167
|
+
artifacts,
|
|
168
|
+
status,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Load run manifest if it exists
|
|
174
|
+
*/
|
|
175
|
+
export function loadManifest(runDir: string): RunManifest | null {
|
|
176
|
+
const manifestPath = join(runDir, 'manifest.json');
|
|
177
|
+
|
|
178
|
+
if (!existsSync(manifestPath)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const content = readFileSync(manifestPath, 'utf-8');
|
|
184
|
+
return JSON.parse(content) as RunManifest;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if a run can be resumed (all artifacts exist and inputs haven't changed)
|
|
192
|
+
*/
|
|
193
|
+
export function canResume(runDir: string, inputs: RunInputs): boolean {
|
|
194
|
+
const manifest = loadManifest(runDir);
|
|
195
|
+
|
|
196
|
+
if (!manifest) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check status
|
|
201
|
+
if (manifest.status !== 'complete') {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check version match
|
|
206
|
+
if (manifest.version !== VERSION) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Verify input hashes still match
|
|
211
|
+
if (inputs.repoRoot && existsSync(inputs.repoRoot)) {
|
|
212
|
+
const currentRepoHash = hashDirectory(inputs.repoRoot);
|
|
213
|
+
if (currentRepoHash !== manifest.inputs.repoHash) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (inputs.eventsPath && existsSync(inputs.eventsPath)) {
|
|
219
|
+
const currentEventsHash = hashFile(inputs.eventsPath);
|
|
220
|
+
if (currentEventsHash !== manifest.inputs.eventsHash) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Verify all artifacts exist
|
|
226
|
+
for (const artifact of manifest.artifacts) {
|
|
227
|
+
if (!existsSync(join(runDir, artifact))) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Load cached artifacts from a previous run
|
|
237
|
+
*/
|
|
238
|
+
export function loadCachedArtifacts(runDir: string): CachedArtifacts {
|
|
239
|
+
const artifacts: CachedArtifacts = {};
|
|
240
|
+
|
|
241
|
+
const files = [
|
|
242
|
+
{ name: 'inferencemap.json', key: 'inferenceMap' },
|
|
243
|
+
{ name: 'insights.json', key: 'insights' },
|
|
244
|
+
{ name: 'joined.json', key: 'joined' },
|
|
245
|
+
{ name: 'runtime.json', key: 'runtime' },
|
|
246
|
+
] as const;
|
|
247
|
+
|
|
248
|
+
for (const file of files) {
|
|
249
|
+
const filePath = join(runDir, file.name);
|
|
250
|
+
if (existsSync(filePath)) {
|
|
251
|
+
try {
|
|
252
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
253
|
+
(artifacts as Record<string, unknown>)[file.key] = JSON.parse(content);
|
|
254
|
+
} catch {
|
|
255
|
+
// Skip corrupted files
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return artifacts;
|
|
261
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { extname } from 'path';
|
|
3
|
+
import { InferenceEvent, RuntimeSummary, ProviderStats, NormalizationOptions, NormalizationResult } from './types.js';
|
|
4
|
+
import { detectFormat, normalizeRuntimeEvents, requiresAgentNormalization } from './format-normalizer.js';
|
|
5
|
+
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// TYPES
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
interface ParseError {
|
|
11
|
+
line: number;
|
|
12
|
+
field: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// HELPERS
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
function validateEvent(data: unknown, lineNum: number): InferenceEvent {
|
|
21
|
+
if (typeof data !== 'object' || data === null) {
|
|
22
|
+
throw new Error(`Line ${lineNum}: Expected object, got ${typeof data}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const obj = data as Record<string, unknown>;
|
|
26
|
+
const errors: string[] = [];
|
|
27
|
+
|
|
28
|
+
// Required fields
|
|
29
|
+
if (typeof obj.id !== 'string') {
|
|
30
|
+
errors.push(`Missing or invalid 'id' field`);
|
|
31
|
+
}
|
|
32
|
+
if (typeof obj.ts !== 'string') {
|
|
33
|
+
errors.push(`Missing or invalid 'ts' field`);
|
|
34
|
+
}
|
|
35
|
+
if (typeof obj.provider !== 'string') {
|
|
36
|
+
errors.push(`Missing or invalid 'provider' field`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof obj.model !== 'string') {
|
|
39
|
+
errors.push(`Missing or invalid 'model' field`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof obj.input_tokens !== 'number') {
|
|
42
|
+
errors.push(`Missing or invalid 'input_tokens' field`);
|
|
43
|
+
}
|
|
44
|
+
if (typeof obj.output_tokens !== 'number') {
|
|
45
|
+
errors.push(`Missing or invalid 'output_tokens' field`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof obj.latency_ms !== 'number') {
|
|
48
|
+
errors.push(`Missing or invalid 'latency_ms' field`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
throw new Error(`Line ${lineNum}: ${errors.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Build event with optional fields
|
|
56
|
+
const event: InferenceEvent = {
|
|
57
|
+
id: obj.id as string,
|
|
58
|
+
ts: obj.ts as string,
|
|
59
|
+
provider: obj.provider as InferenceEvent['provider'],
|
|
60
|
+
model: obj.model as string,
|
|
61
|
+
input_tokens: obj.input_tokens as number,
|
|
62
|
+
output_tokens: obj.output_tokens as number,
|
|
63
|
+
latency_ms: obj.latency_ms as number,
|
|
64
|
+
intent: obj.intent as string | undefined,
|
|
65
|
+
callsite_id: obj.callsite_id as string | undefined,
|
|
66
|
+
// Runtime pattern fields
|
|
67
|
+
streaming: typeof obj.streaming === 'boolean' ? obj.streaming : undefined,
|
|
68
|
+
ttft_ms: typeof obj.ttft_ms === 'number' ? obj.ttft_ms : undefined,
|
|
69
|
+
batch_size: typeof obj.batch_size === 'number' ? obj.batch_size : undefined,
|
|
70
|
+
batch_id: typeof obj.batch_id === 'string' ? obj.batch_id : undefined,
|
|
71
|
+
cached: typeof obj.cached === 'boolean' ? obj.cached : undefined,
|
|
72
|
+
retry_count: typeof obj.retry_count === 'number' ? obj.retry_count : undefined,
|
|
73
|
+
fallback_used: typeof obj.fallback_used === 'boolean' ? obj.fallback_used : undefined,
|
|
74
|
+
original_model: typeof obj.original_model === 'string' ? obj.original_model : undefined,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Infer streaming if not explicitly provided but ttft_ms is present
|
|
78
|
+
if (event.streaming === undefined && event.ttft_ms !== undefined) {
|
|
79
|
+
event.streaming = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return event;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Detect streaming based on heuristics when not explicitly provided.
|
|
87
|
+
*
|
|
88
|
+
* Heuristics:
|
|
89
|
+
* 1. If ttft_ms is present and much smaller than total latency → streaming
|
|
90
|
+
* 2. If output_tokens/latency_ms ratio suggests real-time generation → likely streaming
|
|
91
|
+
* 3. If latency pattern shows consistent per-token timing → streaming
|
|
92
|
+
*/
|
|
93
|
+
function inferStreamingFromHeuristics(events: InferenceEvent[]): InferenceEvent[] {
|
|
94
|
+
return events.map(event => {
|
|
95
|
+
// If streaming is already known, skip
|
|
96
|
+
if (event.streaming !== undefined) {
|
|
97
|
+
return event;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Heuristic 1: If ttft_ms present and < 50% of total latency, likely streaming
|
|
101
|
+
if (event.ttft_ms !== undefined) {
|
|
102
|
+
event.streaming = event.ttft_ms < event.latency_ms * 0.5;
|
|
103
|
+
return event;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Heuristic 2: Calculate tokens per second
|
|
107
|
+
// Streaming typically shows 20-150 TPS, non-streaming shows all tokens at once
|
|
108
|
+
const outputTokens = event.output_tokens;
|
|
109
|
+
const latencySeconds = event.latency_ms / 1000;
|
|
110
|
+
const tps = outputTokens / latencySeconds;
|
|
111
|
+
|
|
112
|
+
// Very high TPS (>200) with significant output suggests non-streaming (batch return)
|
|
113
|
+
// Very low TPS (<5) with significant output suggests non-streaming (slow model, one response)
|
|
114
|
+
// TPS in 20-150 range with reasonable output suggests streaming
|
|
115
|
+
if (outputTokens > 50) {
|
|
116
|
+
if (tps >= 20 && tps <= 150) {
|
|
117
|
+
// Likely streaming - consistent token generation rate
|
|
118
|
+
event.streaming = true;
|
|
119
|
+
} else if (tps > 200) {
|
|
120
|
+
// Likely non-streaming - tokens returned all at once (cached or fast response)
|
|
121
|
+
event.streaming = false;
|
|
122
|
+
}
|
|
123
|
+
// If TPS < 20, could be either - leave as undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return event;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseJSONL(content: string): InferenceEvent[] {
|
|
131
|
+
const lines = content.trim().split('\n').filter(l => l.trim());
|
|
132
|
+
const events: InferenceEvent[] = [];
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
135
|
+
const line = lines[i].trim();
|
|
136
|
+
if (!line) continue;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const data = JSON.parse(line);
|
|
140
|
+
events.push(validateEvent(data, i + 1));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof SyntaxError) {
|
|
143
|
+
throw new Error(`Line ${i + 1}: Invalid JSON`);
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return events;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseJSONArray(content: string): InferenceEvent[] {
|
|
153
|
+
let data: unknown[];
|
|
154
|
+
try {
|
|
155
|
+
data = JSON.parse(content);
|
|
156
|
+
} catch {
|
|
157
|
+
throw new Error('Invalid JSON');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!Array.isArray(data)) {
|
|
161
|
+
throw new Error('Expected JSON array');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return data.map((item, i) => validateEvent(item, i + 1));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseCSV(content: string): InferenceEvent[] {
|
|
168
|
+
const lines = content.trim().split('\n');
|
|
169
|
+
if (lines.length < 2) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const headers = lines[0].split(',').map(h => h.trim());
|
|
174
|
+
const events: InferenceEvent[] = [];
|
|
175
|
+
|
|
176
|
+
for (let i = 1; i < lines.length; i++) {
|
|
177
|
+
const values = lines[i].split(',').map(v => v.trim());
|
|
178
|
+
const obj: Record<string, unknown> = {};
|
|
179
|
+
|
|
180
|
+
for (let j = 0; j < headers.length; j++) {
|
|
181
|
+
const header = headers[j];
|
|
182
|
+
const value = values[j];
|
|
183
|
+
|
|
184
|
+
// Convert numeric fields
|
|
185
|
+
if (['input_tokens', 'output_tokens', 'latency_ms'].includes(header)) {
|
|
186
|
+
obj[header] = parseFloat(value);
|
|
187
|
+
} else {
|
|
188
|
+
obj[header] = value;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
events.push(validateEvent(obj, i + 1));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return events;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// =============================================================================
|
|
199
|
+
// PUBLIC API
|
|
200
|
+
// =============================================================================
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extended parse result with normalization metadata.
|
|
204
|
+
*/
|
|
205
|
+
export interface ParseResult {
|
|
206
|
+
events: InferenceEvent[];
|
|
207
|
+
normalization?: NormalizationResult;
|
|
208
|
+
warnings: string[];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse runtime events from a file.
|
|
213
|
+
*
|
|
214
|
+
* This function implements the format detection pipeline (PRD §6.4):
|
|
215
|
+
* 1. Try direct parsing for known formats (JSONL, JSON, CSV)
|
|
216
|
+
* 2. Fall back to agent-based normalization for unknown formats
|
|
217
|
+
* 3. Apply streaming inference heuristics
|
|
218
|
+
*
|
|
219
|
+
* @param path - Path to the events file
|
|
220
|
+
* @param options - Normalization options (format hints, mappings, etc.)
|
|
221
|
+
*/
|
|
222
|
+
export async function parseEvents(
|
|
223
|
+
path: string,
|
|
224
|
+
options: NormalizationOptions = {},
|
|
225
|
+
): Promise<InferenceEvent[]> {
|
|
226
|
+
const result = await parseEventsWithMetadata(path, options);
|
|
227
|
+
return result.events;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Parse runtime events with full normalization metadata.
|
|
232
|
+
* Use this for detailed reporting on format detection and field mappings.
|
|
233
|
+
*/
|
|
234
|
+
export async function parseEventsWithMetadata(
|
|
235
|
+
path: string,
|
|
236
|
+
options: NormalizationOptions = {},
|
|
237
|
+
): Promise<ParseResult> {
|
|
238
|
+
if (!existsSync(path)) {
|
|
239
|
+
throw new Error(`File not found: ${path}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const content = readFileSync(path, 'utf-8');
|
|
243
|
+
const ext = extname(path).toLowerCase();
|
|
244
|
+
const warnings: string[] = [];
|
|
245
|
+
|
|
246
|
+
// Step 1: Try direct parsing for standard formats
|
|
247
|
+
if (!options.format_hint) {
|
|
248
|
+
try {
|
|
249
|
+
let events: InferenceEvent[] | null = null;
|
|
250
|
+
|
|
251
|
+
switch (ext) {
|
|
252
|
+
case '.jsonl':
|
|
253
|
+
case '.ndjson':
|
|
254
|
+
events = parseJSONL(content);
|
|
255
|
+
break;
|
|
256
|
+
case '.json':
|
|
257
|
+
// Could be JSON array or complex format (OTEL, Jaeger, etc.)
|
|
258
|
+
if (content.trim().startsWith('[')) {
|
|
259
|
+
try {
|
|
260
|
+
events = parseJSONArray(content);
|
|
261
|
+
} catch {
|
|
262
|
+
// JSON array but not InferenceEvent schema - needs normalization
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
case '.csv':
|
|
267
|
+
events = parseCSV(content);
|
|
268
|
+
break;
|
|
269
|
+
case '.tsv':
|
|
270
|
+
events = parseTSV(content);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (events && events.length > 0) {
|
|
275
|
+
// Direct parse succeeded
|
|
276
|
+
return {
|
|
277
|
+
events: inferStreamingFromHeuristics(events),
|
|
278
|
+
warnings,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
// Direct parse failed - will try format normalization
|
|
283
|
+
warnings.push(`Direct parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Step 2: Detect format and check if agent normalization is needed
|
|
288
|
+
const detection = detectFormat(content, path);
|
|
289
|
+
|
|
290
|
+
// If user provided strict mode and we need agent, fail early
|
|
291
|
+
if (options.strict && detection.requires_agent) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Format "${detection.format_type}" requires agent normalization. ` +
|
|
294
|
+
`Use --format to specify the format, or remove --strict flag.`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// If direct-parse format detected with low confidence, warn but try
|
|
299
|
+
if (!detection.requires_agent && detection.confidence < 0.9) {
|
|
300
|
+
warnings.push(
|
|
301
|
+
`Format detection confidence is ${(detection.confidence * 100).toFixed(0)}%. ` +
|
|
302
|
+
`Results may be incomplete.`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Step 3: Use format normalizer for complex formats
|
|
307
|
+
if (detection.requires_agent || options.format_hint) {
|
|
308
|
+
const { events, normalization, errors } = await normalizeRuntimeEvents(content, options);
|
|
309
|
+
|
|
310
|
+
if (errors.length > 0 && options.strict) {
|
|
311
|
+
throw new Error(`Normalization errors: ${errors.slice(0, 3).join('; ')}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
warnings.push(...normalization.warnings);
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
events: inferStreamingFromHeuristics(events),
|
|
318
|
+
normalization,
|
|
319
|
+
warnings,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Step 4: Final fallback - try auto-detection
|
|
324
|
+
let events: InferenceEvent[];
|
|
325
|
+
|
|
326
|
+
if (content.trim().startsWith('[')) {
|
|
327
|
+
events = parseJSONArray(content);
|
|
328
|
+
} else if (content.trim().startsWith('{')) {
|
|
329
|
+
events = parseJSONL(content);
|
|
330
|
+
} else {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Unknown file format: ${ext}. ` +
|
|
333
|
+
`Supported formats: .jsonl, .json, .csv, .tsv, or observability exports (OTEL, Jaeger, Zipkin). ` +
|
|
334
|
+
`Use --format to specify the format type.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
events: inferStreamingFromHeuristics(events),
|
|
340
|
+
warnings,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Parse TSV (tab-separated values) format.
|
|
346
|
+
*/
|
|
347
|
+
function parseTSV(content: string): InferenceEvent[] {
|
|
348
|
+
const lines = content.trim().split('\n');
|
|
349
|
+
if (lines.length < 2) {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const headers = lines[0].split('\t').map(h => h.trim());
|
|
354
|
+
const events: InferenceEvent[] = [];
|
|
355
|
+
|
|
356
|
+
for (let i = 1; i < lines.length; i++) {
|
|
357
|
+
const values = lines[i].split('\t').map(v => v.trim());
|
|
358
|
+
const obj: Record<string, unknown> = {};
|
|
359
|
+
|
|
360
|
+
for (let j = 0; j < headers.length; j++) {
|
|
361
|
+
const header = headers[j];
|
|
362
|
+
const value = values[j];
|
|
363
|
+
|
|
364
|
+
// Convert numeric fields
|
|
365
|
+
if (['input_tokens', 'output_tokens', 'latency_ms', 'ttft_ms', 'batch_size', 'retry_count'].includes(header)) {
|
|
366
|
+
obj[header] = parseFloat(value);
|
|
367
|
+
} else if (['streaming', 'cached', 'fallback_used'].includes(header)) {
|
|
368
|
+
obj[header] = value.toLowerCase() === 'true';
|
|
369
|
+
} else {
|
|
370
|
+
obj[header] = value;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
events.push(validateEvent(obj, i + 1));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return events;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function percentile(values: number[], p: number): number {
|
|
381
|
+
if (values.length === 0) return 0;
|
|
382
|
+
|
|
383
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
384
|
+
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|
385
|
+
return sorted[Math.max(0, index)];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function aggregate(events: InferenceEvent[]): RuntimeSummary {
|
|
389
|
+
if (events.length === 0) {
|
|
390
|
+
return {
|
|
391
|
+
totalEvents: 0,
|
|
392
|
+
byProvider: {},
|
|
393
|
+
byModel: {},
|
|
394
|
+
global: { p50: 0, p95: 0, p99: 0 },
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const byProvider: Record<string, InferenceEvent[]> = {};
|
|
399
|
+
const byModel: Record<string, InferenceEvent[]> = {};
|
|
400
|
+
const allLatencies: number[] = [];
|
|
401
|
+
|
|
402
|
+
for (const event of events) {
|
|
403
|
+
// Group by provider
|
|
404
|
+
if (!byProvider[event.provider]) {
|
|
405
|
+
byProvider[event.provider] = [];
|
|
406
|
+
}
|
|
407
|
+
byProvider[event.provider].push(event);
|
|
408
|
+
|
|
409
|
+
// Group by model
|
|
410
|
+
if (!byModel[event.model]) {
|
|
411
|
+
byModel[event.model] = [];
|
|
412
|
+
}
|
|
413
|
+
byModel[event.model].push(event);
|
|
414
|
+
|
|
415
|
+
allLatencies.push(event.latency_ms);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const computeStats = (group: InferenceEvent[]): ProviderStats => {
|
|
419
|
+
const latencies = group.map(e => e.latency_ms);
|
|
420
|
+
return {
|
|
421
|
+
calls: group.length,
|
|
422
|
+
tokens_in: group.reduce((sum, e) => sum + e.input_tokens, 0),
|
|
423
|
+
tokens_out: group.reduce((sum, e) => sum + e.output_tokens, 0),
|
|
424
|
+
latency_p50: percentile(latencies, 50),
|
|
425
|
+
latency_p95: percentile(latencies, 95),
|
|
426
|
+
latency_p99: percentile(latencies, 99),
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const providerStats: Record<string, ProviderStats> = {};
|
|
431
|
+
for (const [provider, group] of Object.entries(byProvider)) {
|
|
432
|
+
providerStats[provider] = computeStats(group);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const modelStats: Record<string, ProviderStats> = {};
|
|
436
|
+
for (const [model, group] of Object.entries(byModel)) {
|
|
437
|
+
modelStats[model] = computeStats(group);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
totalEvents: events.length,
|
|
442
|
+
byProvider: providerStats,
|
|
443
|
+
byModel: modelStats,
|
|
444
|
+
global: {
|
|
445
|
+
p50: percentile(allLatencies, 50),
|
|
446
|
+
p95: percentile(allLatencies, 95),
|
|
447
|
+
p99: percentile(allLatencies, 99),
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|