@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
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Calculator Tests
|
|
3
|
+
* Per Test Cases v1.9.3 - Core Analysis Tests
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect } from 'vitest';
|
|
6
|
+
|
|
7
|
+
// Types for cost calculation
|
|
8
|
+
interface ModelPricing {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
inputPer1M: number; // Cost per 1M input tokens
|
|
12
|
+
outputPer1M: number; // Cost per 1M output tokens
|
|
13
|
+
cachedInputPer1M?: number; // Cost for cached input tokens
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface TokenUsage {
|
|
17
|
+
inputTokens: number;
|
|
18
|
+
outputTokens: number;
|
|
19
|
+
cachedTokens?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CostEstimate {
|
|
23
|
+
inputCost: number;
|
|
24
|
+
outputCost: number;
|
|
25
|
+
cachedCost: number;
|
|
26
|
+
totalCost: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Model pricing database (subset from LiteLLM pricing)
|
|
30
|
+
const MODEL_PRICING: ModelPricing[] = [
|
|
31
|
+
// Anthropic
|
|
32
|
+
{ provider: 'anthropic', model: 'claude-opus-4-20250514', inputPer1M: 15, outputPer1M: 75, cachedInputPer1M: 1.5 },
|
|
33
|
+
{ provider: 'anthropic', model: 'claude-sonnet-4-20250514', inputPer1M: 3, outputPer1M: 15, cachedInputPer1M: 0.3 },
|
|
34
|
+
{ provider: 'anthropic', model: 'claude-3-5-sonnet-20241022', inputPer1M: 3, outputPer1M: 15, cachedInputPer1M: 0.3 },
|
|
35
|
+
{ provider: 'anthropic', model: 'claude-haiku-3-20240307', inputPer1M: 0.25, outputPer1M: 1.25, cachedInputPer1M: 0.03 },
|
|
36
|
+
// OpenAI
|
|
37
|
+
{ provider: 'openai', model: 'gpt-4o', inputPer1M: 2.5, outputPer1M: 10, cachedInputPer1M: 1.25 },
|
|
38
|
+
{ provider: 'openai', model: 'gpt-4o-mini', inputPer1M: 0.15, outputPer1M: 0.6, cachedInputPer1M: 0.075 },
|
|
39
|
+
{ provider: 'openai', model: 'gpt-4-turbo', inputPer1M: 10, outputPer1M: 30 },
|
|
40
|
+
{ provider: 'openai', model: 'o1', inputPer1M: 15, outputPer1M: 60, cachedInputPer1M: 7.5 },
|
|
41
|
+
{ provider: 'openai', model: 'o1-mini', inputPer1M: 1.1, outputPer1M: 4.4, cachedInputPer1M: 0.55 },
|
|
42
|
+
// Embeddings
|
|
43
|
+
{ provider: 'openai', model: 'text-embedding-3-small', inputPer1M: 0.02, outputPer1M: 0 },
|
|
44
|
+
{ provider: 'openai', model: 'text-embedding-3-large', inputPer1M: 0.13, outputPer1M: 0 },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Cost calculation functions
|
|
48
|
+
function getPricing(provider: string, model: string): ModelPricing | null {
|
|
49
|
+
// Try exact match first
|
|
50
|
+
let pricing = MODEL_PRICING.find(p => p.provider === provider && p.model === model);
|
|
51
|
+
if (pricing) return pricing;
|
|
52
|
+
|
|
53
|
+
// Try partial match (model name contains)
|
|
54
|
+
pricing = MODEL_PRICING.find(p => p.provider === provider && model.includes(p.model));
|
|
55
|
+
if (pricing) return pricing;
|
|
56
|
+
|
|
57
|
+
// Try partial match (pricing model contains model name)
|
|
58
|
+
pricing = MODEL_PRICING.find(p => p.provider === provider && p.model.includes(model));
|
|
59
|
+
return pricing || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function calculateCost(pricing: ModelPricing, usage: TokenUsage): CostEstimate {
|
|
63
|
+
const inputCost = (usage.inputTokens / 1_000_000) * pricing.inputPer1M;
|
|
64
|
+
const outputCost = (usage.outputTokens / 1_000_000) * pricing.outputPer1M;
|
|
65
|
+
const cachedCost = usage.cachedTokens && pricing.cachedInputPer1M
|
|
66
|
+
? (usage.cachedTokens / 1_000_000) * pricing.cachedInputPer1M
|
|
67
|
+
: 0;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
inputCost,
|
|
71
|
+
outputCost,
|
|
72
|
+
cachedCost,
|
|
73
|
+
totalCost: inputCost + outputCost + cachedCost,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function estimateMonthlyCost(
|
|
78
|
+
pricing: ModelPricing,
|
|
79
|
+
usage: TokenUsage,
|
|
80
|
+
requestsPerDay: number
|
|
81
|
+
): number {
|
|
82
|
+
const perRequest = calculateCost(pricing, usage).totalCost;
|
|
83
|
+
return perRequest * requestsPerDay * 30;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function compareCosts(
|
|
87
|
+
usage: TokenUsage,
|
|
88
|
+
model1: { provider: string; model: string },
|
|
89
|
+
model2: { provider: string; model: string }
|
|
90
|
+
): { savings: number; percentSaved: number } | null {
|
|
91
|
+
const pricing1 = getPricing(model1.provider, model1.model);
|
|
92
|
+
const pricing2 = getPricing(model2.provider, model2.model);
|
|
93
|
+
|
|
94
|
+
if (!pricing1 || !pricing2) return null;
|
|
95
|
+
|
|
96
|
+
const cost1 = calculateCost(pricing1, usage).totalCost;
|
|
97
|
+
const cost2 = calculateCost(pricing2, usage).totalCost;
|
|
98
|
+
|
|
99
|
+
const savings = cost1 - cost2;
|
|
100
|
+
const percentSaved = cost1 > 0 ? (savings / cost1) * 100 : 0;
|
|
101
|
+
|
|
102
|
+
return { savings, percentSaved };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe('Model Pricing Lookup', () => {
|
|
106
|
+
test('Finds exact model match', () => {
|
|
107
|
+
const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514');
|
|
108
|
+
expect(pricing).not.toBeNull();
|
|
109
|
+
expect(pricing?.inputPer1M).toBe(3);
|
|
110
|
+
expect(pricing?.outputPer1M).toBe(15);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('Returns null for unknown model', () => {
|
|
114
|
+
const pricing = getPricing('anthropic', 'unknown-model');
|
|
115
|
+
expect(pricing).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('Returns null for unknown provider', () => {
|
|
119
|
+
const pricing = getPricing('unknown-provider', 'gpt-4');
|
|
120
|
+
expect(pricing).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('Finds OpenAI models', () => {
|
|
124
|
+
const pricing = getPricing('openai', 'gpt-4o');
|
|
125
|
+
expect(pricing).not.toBeNull();
|
|
126
|
+
expect(pricing?.inputPer1M).toBe(2.5);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('Finds embedding models', () => {
|
|
130
|
+
const pricing = getPricing('openai', 'text-embedding-3-small');
|
|
131
|
+
expect(pricing).not.toBeNull();
|
|
132
|
+
expect(pricing?.outputPer1M).toBe(0); // Embeddings have no output cost
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('Cost Calculation', () => {
|
|
137
|
+
test('Calculates cost for typical usage', () => {
|
|
138
|
+
const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
|
|
139
|
+
const usage: TokenUsage = {
|
|
140
|
+
inputTokens: 1000,
|
|
141
|
+
outputTokens: 500,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const cost = calculateCost(pricing, usage);
|
|
145
|
+
|
|
146
|
+
// 1000 input tokens at $3/1M = $0.003
|
|
147
|
+
expect(cost.inputCost).toBeCloseTo(0.003, 6);
|
|
148
|
+
// 500 output tokens at $15/1M = $0.0075
|
|
149
|
+
expect(cost.outputCost).toBeCloseTo(0.0075, 6);
|
|
150
|
+
expect(cost.totalCost).toBeCloseTo(0.0105, 6);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('Calculates cost with cached tokens', () => {
|
|
154
|
+
const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
|
|
155
|
+
const usage: TokenUsage = {
|
|
156
|
+
inputTokens: 500,
|
|
157
|
+
outputTokens: 500,
|
|
158
|
+
cachedTokens: 500,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const cost = calculateCost(pricing, usage);
|
|
162
|
+
|
|
163
|
+
// 500 input tokens at $3/1M = $0.0015
|
|
164
|
+
expect(cost.inputCost).toBeCloseTo(0.0015, 6);
|
|
165
|
+
// 500 cached tokens at $0.30/1M = $0.00015
|
|
166
|
+
expect(cost.cachedCost).toBeCloseTo(0.00015, 6);
|
|
167
|
+
expect(cost.totalCost).toBeCloseTo(0.0015 + 0.0075 + 0.00015, 6);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('Handles zero tokens', () => {
|
|
171
|
+
const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
|
|
172
|
+
const usage: TokenUsage = {
|
|
173
|
+
inputTokens: 0,
|
|
174
|
+
outputTokens: 0,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const cost = calculateCost(pricing, usage);
|
|
178
|
+
|
|
179
|
+
expect(cost.totalCost).toBe(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('Handles large token counts', () => {
|
|
183
|
+
const pricing = getPricing('openai', 'gpt-4o')!;
|
|
184
|
+
const usage: TokenUsage = {
|
|
185
|
+
inputTokens: 1_000_000,
|
|
186
|
+
outputTokens: 500_000,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const cost = calculateCost(pricing, usage);
|
|
190
|
+
|
|
191
|
+
// 1M input at $2.5/1M = $2.5
|
|
192
|
+
expect(cost.inputCost).toBeCloseTo(2.5, 2);
|
|
193
|
+
// 500K output at $10/1M = $5
|
|
194
|
+
expect(cost.outputCost).toBeCloseTo(5, 2);
|
|
195
|
+
expect(cost.totalCost).toBeCloseTo(7.5, 2);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Monthly Cost Estimation', () => {
|
|
200
|
+
test('Estimates monthly cost correctly', () => {
|
|
201
|
+
const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
|
|
202
|
+
const usage: TokenUsage = {
|
|
203
|
+
inputTokens: 2000,
|
|
204
|
+
outputTokens: 1000,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const monthlyCost = estimateMonthlyCost(pricing, usage, 100); // 100 requests/day
|
|
208
|
+
|
|
209
|
+
// Per request: (2000 * 3 + 1000 * 15) / 1M = 0.021
|
|
210
|
+
// Monthly: 0.021 * 100 * 30 = 63
|
|
211
|
+
expect(monthlyCost).toBeCloseTo(63, 0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('Handles zero requests', () => {
|
|
215
|
+
const pricing = getPricing('anthropic', 'claude-sonnet-4-20250514')!;
|
|
216
|
+
const usage: TokenUsage = { inputTokens: 1000, outputTokens: 1000 };
|
|
217
|
+
|
|
218
|
+
const monthlyCost = estimateMonthlyCost(pricing, usage, 0);
|
|
219
|
+
|
|
220
|
+
expect(monthlyCost).toBe(0);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('Cost Comparison', () => {
|
|
225
|
+
test('Compares costs between models', () => {
|
|
226
|
+
const usage: TokenUsage = {
|
|
227
|
+
inputTokens: 10000,
|
|
228
|
+
outputTokens: 5000,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const comparison = compareCosts(
|
|
232
|
+
usage,
|
|
233
|
+
{ provider: 'anthropic', model: 'claude-opus-4-20250514' },
|
|
234
|
+
{ provider: 'anthropic', model: 'claude-sonnet-4-20250514' }
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(comparison).not.toBeNull();
|
|
238
|
+
expect(comparison!.savings).toBeGreaterThan(0); // Opus is more expensive
|
|
239
|
+
expect(comparison!.percentSaved).toBeGreaterThan(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('Shows negative savings when upgrading to expensive model', () => {
|
|
243
|
+
const usage: TokenUsage = {
|
|
244
|
+
inputTokens: 10000,
|
|
245
|
+
outputTokens: 5000,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const comparison = compareCosts(
|
|
249
|
+
usage,
|
|
250
|
+
{ provider: 'anthropic', model: 'claude-sonnet-4-20250514' },
|
|
251
|
+
{ provider: 'anthropic', model: 'claude-opus-4-20250514' }
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(comparison).not.toBeNull();
|
|
255
|
+
expect(comparison!.savings).toBeLessThan(0); // Sonnet is cheaper
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('Returns null for unknown models', () => {
|
|
259
|
+
const usage: TokenUsage = { inputTokens: 1000, outputTokens: 1000 };
|
|
260
|
+
|
|
261
|
+
const comparison = compareCosts(
|
|
262
|
+
usage,
|
|
263
|
+
{ provider: 'anthropic', model: 'unknown' },
|
|
264
|
+
{ provider: 'anthropic', model: 'claude-sonnet-4-20250514' }
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
expect(comparison).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('Calculates significant savings for haiku vs opus', () => {
|
|
271
|
+
const usage: TokenUsage = {
|
|
272
|
+
inputTokens: 100000,
|
|
273
|
+
outputTokens: 50000,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const comparison = compareCosts(
|
|
277
|
+
usage,
|
|
278
|
+
{ provider: 'anthropic', model: 'claude-opus-4-20250514' },
|
|
279
|
+
{ provider: 'anthropic', model: 'claude-haiku-3-20240307' }
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(comparison).not.toBeNull();
|
|
283
|
+
// Haiku should be ~60x cheaper for input, ~60x cheaper for output
|
|
284
|
+
expect(comparison!.percentSaved).toBeGreaterThan(90);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('Embedding Costs', () => {
|
|
289
|
+
test('Calculates embedding costs (output is zero)', () => {
|
|
290
|
+
const pricing = getPricing('openai', 'text-embedding-3-small')!;
|
|
291
|
+
const usage: TokenUsage = {
|
|
292
|
+
inputTokens: 8000, // Typical embedding request
|
|
293
|
+
outputTokens: 0,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const cost = calculateCost(pricing, usage);
|
|
297
|
+
|
|
298
|
+
// 8000 tokens at $0.02/1M = $0.00016
|
|
299
|
+
expect(cost.inputCost).toBeCloseTo(0.00016, 6);
|
|
300
|
+
expect(cost.outputCost).toBe(0);
|
|
301
|
+
expect(cost.totalCost).toBeCloseTo(0.00016, 6);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit System Tests
|
|
3
|
+
* Per Test Cases v1.9.3 - Critical Path Tests
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
interface CreditPack {
|
|
9
|
+
credits: number;
|
|
10
|
+
purchasedAt: string;
|
|
11
|
+
expired?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface AnalysisOptions {
|
|
15
|
+
static?: boolean;
|
|
16
|
+
runtime?: boolean;
|
|
17
|
+
templates?: boolean;
|
|
18
|
+
historical?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Functions under test (to be implemented in src/credits.ts)
|
|
22
|
+
function calculateCost(options: AnalysisOptions): number {
|
|
23
|
+
let cost = 0;
|
|
24
|
+
if (options.static) cost += 1;
|
|
25
|
+
if (options.runtime) cost += 1;
|
|
26
|
+
if (options.templates) cost += 1;
|
|
27
|
+
if (options.historical) cost += 1;
|
|
28
|
+
return cost;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function consumeCredits(packs: CreditPack[], amount: number): { consumed: number; remaining: number } {
|
|
32
|
+
let remaining = amount;
|
|
33
|
+
|
|
34
|
+
// Sort by purchase date (FIFO)
|
|
35
|
+
const sortedPacks = [...packs].sort(
|
|
36
|
+
(a, b) => new Date(a.purchasedAt).getTime() - new Date(b.purchasedAt).getTime()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
for (const pack of sortedPacks) {
|
|
40
|
+
if (pack.expired) continue;
|
|
41
|
+
|
|
42
|
+
const toConsume = Math.min(pack.credits, remaining);
|
|
43
|
+
pack.credits -= toConsume;
|
|
44
|
+
remaining -= toConsume;
|
|
45
|
+
|
|
46
|
+
if (remaining === 0) break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (remaining > 0) {
|
|
50
|
+
throw new Error('Insufficient credits');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { consumed: amount, remaining: 0 };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isExpired(purchasedAt: string, expiryMonths: number = 6): boolean {
|
|
57
|
+
const purchaseDate = new Date(purchasedAt);
|
|
58
|
+
const expiryDate = new Date(purchaseDate);
|
|
59
|
+
expiryDate.setMonth(expiryDate.getMonth() + expiryMonths);
|
|
60
|
+
return new Date() > expiryDate;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getTotalCredits(packs: CreditPack[]): number {
|
|
64
|
+
return packs
|
|
65
|
+
.filter(pack => !pack.expired)
|
|
66
|
+
.reduce((sum, pack) => sum + pack.credits, 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Tests
|
|
70
|
+
describe('Credit Calculation', () => {
|
|
71
|
+
test('Static-only analysis costs 1 credit', () => {
|
|
72
|
+
expect(calculateCost({ static: true, runtime: false })).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('Runtime analysis costs 2 credits', () => {
|
|
76
|
+
expect(calculateCost({ static: true, runtime: true })).toBe(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('Template suggestions cost 3 credits', () => {
|
|
80
|
+
expect(calculateCost({ static: true, runtime: true, templates: true })).toBe(3);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('Historical comparison adds 1 credit', () => {
|
|
84
|
+
expect(calculateCost({ static: true, historical: true })).toBe(2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('Full analysis costs 4 credits', () => {
|
|
88
|
+
expect(calculateCost({ static: true, runtime: true, templates: true, historical: true })).toBe(4);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('Empty options costs 0 credits', () => {
|
|
92
|
+
expect(calculateCost({})).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('FIFO Consumption', () => {
|
|
97
|
+
test('Oldest credits consumed first', () => {
|
|
98
|
+
const packs: CreditPack[] = [
|
|
99
|
+
{ credits: 50, purchasedAt: '2025-01-01' },
|
|
100
|
+
{ credits: 200, purchasedAt: '2025-06-01' },
|
|
101
|
+
];
|
|
102
|
+
consumeCredits(packs, 30);
|
|
103
|
+
expect(packs[0].credits).toBe(20); // Oldest reduced
|
|
104
|
+
expect(packs[1].credits).toBe(200); // Newer untouched
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('Expired credits skipped', () => {
|
|
108
|
+
const packs: CreditPack[] = [
|
|
109
|
+
{ credits: 50, purchasedAt: '2024-01-01', expired: true },
|
|
110
|
+
{ credits: 200, purchasedAt: '2025-06-01' },
|
|
111
|
+
];
|
|
112
|
+
consumeCredits(packs, 30);
|
|
113
|
+
expect(packs[0].credits).toBe(50); // Expired untouched
|
|
114
|
+
expect(packs[1].credits).toBe(170); // Active reduced
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('Cannot go negative', () => {
|
|
118
|
+
const packs: CreditPack[] = [{ credits: 10, purchasedAt: '2025-01-01' }];
|
|
119
|
+
expect(() => consumeCredits(packs, 20)).toThrow('Insufficient credits');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('Consumes across multiple packs', () => {
|
|
123
|
+
const packs: CreditPack[] = [
|
|
124
|
+
{ credits: 30, purchasedAt: '2025-01-01' },
|
|
125
|
+
{ credits: 50, purchasedAt: '2025-02-01' },
|
|
126
|
+
];
|
|
127
|
+
consumeCredits(packs, 50);
|
|
128
|
+
expect(packs[0].credits).toBe(0);
|
|
129
|
+
expect(packs[1].credits).toBe(30);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('Exact consumption works', () => {
|
|
133
|
+
const packs: CreditPack[] = [{ credits: 50, purchasedAt: '2025-01-01' }];
|
|
134
|
+
consumeCredits(packs, 50);
|
|
135
|
+
expect(packs[0].credits).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('Credit Expiration', () => {
|
|
140
|
+
test('Credits expire after 6 months', () => {
|
|
141
|
+
const oldDate = new Date();
|
|
142
|
+
oldDate.setMonth(oldDate.getMonth() - 7);
|
|
143
|
+
expect(isExpired(oldDate.toISOString())).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('Recent credits are not expired', () => {
|
|
147
|
+
const recentDate = new Date();
|
|
148
|
+
recentDate.setMonth(recentDate.getMonth() - 1);
|
|
149
|
+
expect(isExpired(recentDate.toISOString())).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('Credits at exactly 6 months are not expired', () => {
|
|
153
|
+
const exactDate = new Date();
|
|
154
|
+
exactDate.setMonth(exactDate.getMonth() - 6);
|
|
155
|
+
exactDate.setDate(exactDate.getDate() + 1); // Just under 6 months
|
|
156
|
+
expect(isExpired(exactDate.toISOString())).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Total Credits', () => {
|
|
161
|
+
test('Sums all non-expired packs', () => {
|
|
162
|
+
const packs: CreditPack[] = [
|
|
163
|
+
{ credits: 50, purchasedAt: '2025-01-01' },
|
|
164
|
+
{ credits: 200, purchasedAt: '2025-06-01' },
|
|
165
|
+
];
|
|
166
|
+
expect(getTotalCredits(packs)).toBe(250);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('Excludes expired packs', () => {
|
|
170
|
+
const packs: CreditPack[] = [
|
|
171
|
+
{ credits: 50, purchasedAt: '2025-01-01', expired: true },
|
|
172
|
+
{ credits: 200, purchasedAt: '2025-06-01' },
|
|
173
|
+
];
|
|
174
|
+
expect(getTotalCredits(packs)).toBe(200);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('Empty packs returns 0', () => {
|
|
178
|
+
expect(getTotalCredits([])).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
});
|