pi-smart-router 0.1.0
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/.pi/extensions/smart-router/command-formatters.ts +167 -0
- package/.pi/extensions/smart-router/commands.ts +242 -0
- package/.pi/extensions/smart-router/dataset-export.ts +142 -0
- package/.pi/extensions/smart-router/delegate-stream.ts +105 -0
- package/.pi/extensions/smart-router/delegation-runtime.ts +355 -0
- package/.pi/extensions/smart-router/extension-setup.ts +115 -0
- package/.pi/extensions/smart-router/fleet-bootstrap.ts +244 -0
- package/.pi/extensions/smart-router/index.ts +115 -0
- package/.pi/extensions/smart-router/package.json +3 -0
- package/.pi/extensions/smart-router/pi-model-scope.ts +55 -0
- package/.pi/extensions/smart-router/pricing-lifecycle.ts +43 -0
- package/.pi/extensions/smart-router/route-and-delegate.ts +562 -0
- package/.pi/extensions/smart-router/routing-context.ts +171 -0
- package/.pi/extensions/smart-router/routing-outcomes.ts +44 -0
- package/.pi/extensions/smart-router/session-lifecycle.ts +140 -0
- package/.pi/extensions/smart-router/stream-delegation.ts +75 -0
- package/.pi/extensions/smart-router/types.ts +90 -0
- package/.pi/extensions/smart-router/utils.ts +58 -0
- package/README.md +593 -0
- package/bin/pi-smart-router.mjs +79 -0
- package/config/.gitkeep +0 -0
- package/config/hydra-projection-weights.json.example +1169 -0
- package/config/models.yaml.example +58 -0
- package/config/p-success-weights.json.example +19 -0
- package/config/routing-calibration.json.example +1605 -0
- package/config/routing-centroids.json.example +1570 -0
- package/config/routing-clusters.yaml.example +41 -0
- package/data/contrib/example.json +25 -0
- package/dist/api/explain/router-explain.d.ts +47 -0
- package/dist/api/explain/router-explain.d.ts.map +1 -0
- package/dist/api/explain/router-explain.js +101 -0
- package/dist/api/explain/router-explain.js.map +1 -0
- package/dist/api/middleware/pi-router-middleware.d.ts +86 -0
- package/dist/api/middleware/pi-router-middleware.d.ts.map +1 -0
- package/dist/api/middleware/pi-router-middleware.js +83 -0
- package/dist/api/middleware/pi-router-middleware.js.map +1 -0
- package/dist/cli/smart-router-cli.d.ts +85 -0
- package/dist/cli/smart-router-cli.d.ts.map +1 -0
- package/dist/cli/smart-router-cli.js +293 -0
- package/dist/cli/smart-router-cli.js.map +1 -0
- package/dist/config/defaults.d.ts +7 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +33 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/models-loader.d.ts +22 -0
- package/dist/config/models-loader.d.ts.map +1 -0
- package/dist/config/models-loader.js +51 -0
- package/dist/config/models-loader.js.map +1 -0
- package/dist/config/pi-model-mapper.d.ts +40 -0
- package/dist/config/pi-model-mapper.d.ts.map +1 -0
- package/dist/config/pi-model-mapper.js +216 -0
- package/dist/config/pi-model-mapper.js.map +1 -0
- package/dist/config/routing-clusters-loader.d.ts +35 -0
- package/dist/config/routing-clusters-loader.d.ts.map +1 -0
- package/dist/config/routing-clusters-loader.js +100 -0
- package/dist/config/routing-clusters-loader.js.map +1 -0
- package/dist/domain/delegation/delegation-context.d.ts +23 -0
- package/dist/domain/delegation/delegation-context.d.ts.map +1 -0
- package/dist/domain/delegation/delegation-context.js +73 -0
- package/dist/domain/delegation/delegation-context.js.map +1 -0
- package/dist/domain/delegation/execution-ledger.d.ts +19 -0
- package/dist/domain/delegation/execution-ledger.d.ts.map +1 -0
- package/dist/domain/delegation/execution-ledger.js +23 -0
- package/dist/domain/delegation/execution-ledger.js.map +1 -0
- package/dist/domain/delegation/output-headroom.d.ts +39 -0
- package/dist/domain/delegation/output-headroom.d.ts.map +1 -0
- package/dist/domain/delegation/output-headroom.js +78 -0
- package/dist/domain/delegation/output-headroom.js.map +1 -0
- package/dist/domain/matching/cluster-matcher.d.ts +69 -0
- package/dist/domain/matching/cluster-matcher.d.ts.map +1 -0
- package/dist/domain/matching/cluster-matcher.js +294 -0
- package/dist/domain/matching/cluster-matcher.js.map +1 -0
- package/dist/domain/matching/embedding-provider.d.ts +21 -0
- package/dist/domain/matching/embedding-provider.d.ts.map +1 -0
- package/dist/domain/matching/embedding-provider.js +42 -0
- package/dist/domain/matching/embedding-provider.js.map +1 -0
- package/dist/domain/matching/hydra-input.d.ts +18 -0
- package/dist/domain/matching/hydra-input.d.ts.map +1 -0
- package/dist/domain/matching/hydra-input.js +40 -0
- package/dist/domain/matching/hydra-input.js.map +1 -0
- package/dist/domain/matching/hydra-matcher.d.ts +103 -0
- package/dist/domain/matching/hydra-matcher.d.ts.map +1 -0
- package/dist/domain/matching/hydra-matcher.js +275 -0
- package/dist/domain/matching/hydra-matcher.js.map +1 -0
- package/dist/domain/pinning/cache-economics.d.ts +53 -0
- package/dist/domain/pinning/cache-economics.d.ts.map +1 -0
- package/dist/domain/pinning/cache-economics.js +72 -0
- package/dist/domain/pinning/cache-economics.js.map +1 -0
- package/dist/domain/pinning/loop-escalation.d.ts +41 -0
- package/dist/domain/pinning/loop-escalation.d.ts.map +1 -0
- package/dist/domain/pinning/loop-escalation.js +137 -0
- package/dist/domain/pinning/loop-escalation.js.map +1 -0
- package/dist/domain/pinning/session-pinner.d.ts +81 -0
- package/dist/domain/pinning/session-pinner.d.ts.map +1 -0
- package/dist/domain/pinning/session-pinner.js +237 -0
- package/dist/domain/pinning/session-pinner.js.map +1 -0
- package/dist/domain/pinning/sub-route-policy.d.ts +41 -0
- package/dist/domain/pinning/sub-route-policy.d.ts.map +1 -0
- package/dist/domain/pinning/sub-route-policy.js +72 -0
- package/dist/domain/pinning/sub-route-policy.js.map +1 -0
- package/dist/domain/pipeline/router-pipeline.d.ts +180 -0
- package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -0
- package/dist/domain/pipeline/router-pipeline.js +807 -0
- package/dist/domain/pipeline/router-pipeline.js.map +1 -0
- package/dist/domain/pipeline/safe-default.d.ts +25 -0
- package/dist/domain/pipeline/safe-default.d.ts.map +1 -0
- package/dist/domain/pipeline/safe-default.js +41 -0
- package/dist/domain/pipeline/safe-default.js.map +1 -0
- package/dist/domain/refactor-guardrails.d.ts +48 -0
- package/dist/domain/refactor-guardrails.d.ts.map +1 -0
- package/dist/domain/refactor-guardrails.js +129 -0
- package/dist/domain/refactor-guardrails.js.map +1 -0
- package/dist/domain/routing/context-fit.d.ts +53 -0
- package/dist/domain/routing/context-fit.d.ts.map +1 -0
- package/dist/domain/routing/context-fit.js +142 -0
- package/dist/domain/routing/context-fit.js.map +1 -0
- package/dist/domain/routing/expected-cost.d.ts +63 -0
- package/dist/domain/routing/expected-cost.d.ts.map +1 -0
- package/dist/domain/routing/expected-cost.js +177 -0
- package/dist/domain/routing/expected-cost.js.map +1 -0
- package/dist/domain/routing/p-success-classifier.d.ts +130 -0
- package/dist/domain/routing/p-success-classifier.d.ts.map +1 -0
- package/dist/domain/routing/p-success-classifier.js +350 -0
- package/dist/domain/routing/p-success-classifier.js.map +1 -0
- package/dist/domain/routing/tier-features.d.ts +70 -0
- package/dist/domain/routing/tier-features.d.ts.map +1 -0
- package/dist/domain/routing/tier-features.js +174 -0
- package/dist/domain/routing/tier-features.js.map +1 -0
- package/dist/domain/routing/tool-history-guard.d.ts +32 -0
- package/dist/domain/routing/tool-history-guard.d.ts.map +1 -0
- package/dist/domain/routing/tool-history-guard.js +110 -0
- package/dist/domain/routing/tool-history-guard.js.map +1 -0
- package/dist/domain/scoring/multi-objective.d.ts +48 -0
- package/dist/domain/scoring/multi-objective.d.ts.map +1 -0
- package/dist/domain/scoring/multi-objective.js +116 -0
- package/dist/domain/scoring/multi-objective.js.map +1 -0
- package/dist/domain/triage/triage-engine.d.ts +43 -0
- package/dist/domain/triage/triage-engine.d.ts.map +1 -0
- package/dist/domain/triage/triage-engine.js +317 -0
- package/dist/domain/triage/triage-engine.js.map +1 -0
- package/dist/domain/triage/turn-envelope.d.ts +17 -0
- package/dist/domain/triage/turn-envelope.d.ts.map +1 -0
- package/dist/domain/triage/turn-envelope.js +81 -0
- package/dist/domain/triage/turn-envelope.js.map +1 -0
- package/dist/domain/types/entities.d.ts +303 -0
- package/dist/domain/types/entities.d.ts.map +1 -0
- package/dist/domain/types/entities.js +6 -0
- package/dist/domain/types/entities.js.map +1 -0
- package/dist/domain/types/index.d.ts +4 -0
- package/dist/domain/types/index.d.ts.map +1 -0
- package/dist/domain/types/index.js +2 -0
- package/dist/domain/types/index.js.map +1 -0
- package/dist/domain/types/schemas.d.ts +346 -0
- package/dist/domain/types/schemas.d.ts.map +1 -0
- package/dist/domain/types/schemas.js +241 -0
- package/dist/domain/types/schemas.js.map +1 -0
- package/dist/domain/types/store-port.d.ts +44 -0
- package/dist/domain/types/store-port.d.ts.map +1 -0
- package/dist/domain/types/store-port.js +6 -0
- package/dist/domain/types/store-port.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/gemini-provider.d.ts +28 -0
- package/dist/infra/gemini-provider.d.ts.map +1 -0
- package/dist/infra/gemini-provider.js +112 -0
- package/dist/infra/gemini-provider.js.map +1 -0
- package/dist/infra/telemetry.d.ts +51 -0
- package/dist/infra/telemetry.d.ts.map +1 -0
- package/dist/infra/telemetry.js +100 -0
- package/dist/infra/telemetry.js.map +1 -0
- package/dist/infrastructure/delegation/provider-error.d.ts +44 -0
- package/dist/infrastructure/delegation/provider-error.d.ts.map +1 -0
- package/dist/infrastructure/delegation/provider-error.js +179 -0
- package/dist/infrastructure/delegation/provider-error.js.map +1 -0
- package/dist/infrastructure/gateway/circuit-breaker.d.ts +68 -0
- package/dist/infrastructure/gateway/circuit-breaker.d.ts.map +1 -0
- package/dist/infrastructure/gateway/circuit-breaker.js +150 -0
- package/dist/infrastructure/gateway/circuit-breaker.js.map +1 -0
- package/dist/infrastructure/gateway/gateway-dispatch.d.ts +138 -0
- package/dist/infrastructure/gateway/gateway-dispatch.d.ts.map +1 -0
- package/dist/infrastructure/gateway/gateway-dispatch.js +308 -0
- package/dist/infrastructure/gateway/gateway-dispatch.js.map +1 -0
- package/dist/infrastructure/hardware/hardware-probe.d.ts +32 -0
- package/dist/infrastructure/hardware/hardware-probe.d.ts.map +1 -0
- package/dist/infrastructure/hardware/hardware-probe.js +199 -0
- package/dist/infrastructure/hardware/hardware-probe.js.map +1 -0
- package/dist/infrastructure/local/local-zero-tier.d.ts +47 -0
- package/dist/infrastructure/local/local-zero-tier.d.ts.map +1 -0
- package/dist/infrastructure/local/local-zero-tier.js +92 -0
- package/dist/infrastructure/local/local-zero-tier.js.map +1 -0
- package/dist/infrastructure/persistence/memory-store.d.ts +31 -0
- package/dist/infrastructure/persistence/memory-store.d.ts.map +1 -0
- package/dist/infrastructure/persistence/memory-store.js +87 -0
- package/dist/infrastructure/persistence/memory-store.js.map +1 -0
- package/dist/infrastructure/persistence/sqlite-store.d.ts +81 -0
- package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -0
- package/dist/infrastructure/persistence/sqlite-store.js +658 -0
- package/dist/infrastructure/persistence/sqlite-store.js.map +1 -0
- package/dist/infrastructure/pricing/litellm-fetch.d.ts +46 -0
- package/dist/infrastructure/pricing/litellm-fetch.d.ts.map +1 -0
- package/dist/infrastructure/pricing/litellm-fetch.js +145 -0
- package/dist/infrastructure/pricing/litellm-fetch.js.map +1 -0
- package/dist/infrastructure/pricing/price-broker.d.ts +56 -0
- package/dist/infrastructure/pricing/price-broker.d.ts.map +1 -0
- package/dist/infrastructure/pricing/price-broker.js +133 -0
- package/dist/infrastructure/pricing/price-broker.js.map +1 -0
- package/dist/infrastructure/pricing/pricing-monitor.d.ts +27 -0
- package/dist/infrastructure/pricing/pricing-monitor.d.ts.map +1 -0
- package/dist/infrastructure/pricing/pricing-monitor.js +46 -0
- package/dist/infrastructure/pricing/pricing-monitor.js.map +1 -0
- package/dist/infrastructure/telemetry/dataset-limits.d.ts +13 -0
- package/dist/infrastructure/telemetry/dataset-limits.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/dataset-limits.js +28 -0
- package/dist/infrastructure/telemetry/dataset-limits.js.map +1 -0
- package/dist/infrastructure/telemetry/dataset-recorder.d.ts +48 -0
- package/dist/infrastructure/telemetry/dataset-recorder.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/dataset-recorder.js +154 -0
- package/dist/infrastructure/telemetry/dataset-recorder.js.map +1 -0
- package/dist/infrastructure/telemetry/outcome-limits.d.ts +12 -0
- package/dist/infrastructure/telemetry/outcome-limits.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/outcome-limits.js +28 -0
- package/dist/infrastructure/telemetry/outcome-limits.js.map +1 -0
- package/dist/infrastructure/telemetry/outcome-recorder.d.ts +29 -0
- package/dist/infrastructure/telemetry/outcome-recorder.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/outcome-recorder.js +49 -0
- package/dist/infrastructure/telemetry/outcome-recorder.js.map +1 -0
- package/dist/infrastructure/telemetry/routing-telemetry.d.ts +115 -0
- package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/routing-telemetry.js +529 -0
- package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -0
- package/dist/infrastructure/telemetry/telemetry-limits.d.ts +16 -0
- package/dist/infrastructure/telemetry/telemetry-limits.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/telemetry-limits.js +35 -0
- package/dist/infrastructure/telemetry/telemetry-limits.js.map +1 -0
- package/package.json +110 -0
- package/skills/router-backlog-orchestrator/SKILL.md +183 -0
- package/skills/router-backlog-orchestrator/references/github-router-issue-template.md +78 -0
- package/skills/router-backlog-orchestrator/references/packet-from-issue.md +99 -0
- package/skills/router-backlog-orchestrator/references/prioritization-rubric.md +65 -0
- package/skills/router-backlog-orchestrator/scripts/collect-backlog.sh +44 -0
- package/specs/001-build-smart-router/contracts/telemetry-contrib.schema.json +151 -0
- package/src/api/explain/.gitkeep +0 -0
- package/src/api/explain/router-explain.ts +146 -0
- package/src/api/middleware/.gitkeep +0 -0
- package/src/api/middleware/pi-router-middleware.ts +169 -0
- package/src/cli/smart-router-cli.ts +436 -0
- package/src/config/.gitkeep +0 -0
- package/src/config/defaults.ts +35 -0
- package/src/config/models-loader.ts +68 -0
- package/src/config/pi-model-mapper.ts +285 -0
- package/src/config/routing-clusters-loader.ts +148 -0
- package/src/domain/delegation/delegation-context.ts +111 -0
- package/src/domain/delegation/execution-ledger.ts +34 -0
- package/src/domain/delegation/output-headroom.ts +131 -0
- package/src/domain/matching/.gitkeep +0 -0
- package/src/domain/matching/cluster-matcher.ts +474 -0
- package/src/domain/matching/embedding-provider.ts +79 -0
- package/src/domain/matching/hydra-input.ts +53 -0
- package/src/domain/matching/hydra-matcher.ts +454 -0
- package/src/domain/pinning/.gitkeep +0 -0
- package/src/domain/pinning/cache-economics.ts +128 -0
- package/src/domain/pinning/loop-escalation.ts +181 -0
- package/src/domain/pinning/session-pinner.ts +388 -0
- package/src/domain/pinning/sub-route-policy.ts +119 -0
- package/src/domain/pipeline/.gitkeep +0 -0
- package/src/domain/pipeline/router-pipeline.ts +1139 -0
- package/src/domain/pipeline/safe-default.ts +70 -0
- package/src/domain/refactor-guardrails.ts +202 -0
- package/src/domain/routing/context-fit.ts +221 -0
- package/src/domain/routing/expected-cost.ts +320 -0
- package/src/domain/routing/p-success-classifier.ts +511 -0
- package/src/domain/routing/tier-features.ts +290 -0
- package/src/domain/routing/tool-history-guard.ts +159 -0
- package/src/domain/scoring/.gitkeep +0 -0
- package/src/domain/scoring/multi-objective.ts +175 -0
- package/src/domain/triage/.gitkeep +0 -0
- package/src/domain/triage/triage-engine.ts +373 -0
- package/src/domain/triage/turn-envelope.ts +104 -0
- package/src/domain/types/.gitkeep +0 -0
- package/src/domain/types/entities.ts +384 -0
- package/src/domain/types/index.ts +41 -0
- package/src/domain/types/schemas.ts +289 -0
- package/src/domain/types/store-port.ts +59 -0
- package/src/index.ts +103 -0
- package/src/infra/gemini-provider.ts +142 -0
- package/src/infra/telemetry.ts +169 -0
- package/src/infrastructure/delegation/provider-error.ts +251 -0
- package/src/infrastructure/gateway/.gitkeep +0 -0
- package/src/infrastructure/gateway/circuit-breaker.ts +196 -0
- package/src/infrastructure/gateway/gateway-dispatch.ts +444 -0
- package/src/infrastructure/hardware/.gitkeep +0 -0
- package/src/infrastructure/hardware/hardware-probe.ts +276 -0
- package/src/infrastructure/local/.gitkeep +0 -0
- package/src/infrastructure/local/local-zero-tier.ts +151 -0
- package/src/infrastructure/persistence/.gitkeep +0 -0
- package/src/infrastructure/persistence/memory-store.ts +122 -0
- package/src/infrastructure/persistence/sqlite-store.ts +900 -0
- package/src/infrastructure/pricing/.gitkeep +0 -0
- package/src/infrastructure/pricing/litellm-fetch.ts +215 -0
- package/src/infrastructure/pricing/price-broker.ts +183 -0
- package/src/infrastructure/pricing/pricing-monitor.ts +65 -0
- package/src/infrastructure/telemetry/.gitkeep +0 -0
- package/src/infrastructure/telemetry/dataset-limits.ts +39 -0
- package/src/infrastructure/telemetry/dataset-recorder.ts +215 -0
- package/src/infrastructure/telemetry/outcome-limits.ts +42 -0
- package/src/infrastructure/telemetry/outcome-recorder.ts +118 -0
- package/src/infrastructure/telemetry/routing-telemetry.ts +824 -0
- package/src/infrastructure/telemetry/telemetry-limits.ts +50 -0
|
File without changes
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteLLM pricing fetch — manual refresh path (SP-045).
|
|
3
|
+
*
|
|
4
|
+
* Fetches the public LiteLLM model pricing JSON and normalizes chat-model
|
|
5
|
+
* rates into PriceCatalog.registry_snapshot entries keyed by model id and
|
|
6
|
+
* provider/model aliases for tri-tier broker lookup.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ModelLimits, PriceCatalog } from '../../domain/types/index.js';
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_LITELLM_PRICING_URL =
|
|
12
|
+
'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
13
|
+
|
|
14
|
+
const CHAT_MODES = new Set(['chat', 'completion']);
|
|
15
|
+
|
|
16
|
+
export interface LitellmFetchResult {
|
|
17
|
+
readonly registry_snapshot: Readonly<Record<string, number>>;
|
|
18
|
+
readonly registry_limits_snapshot: Readonly<Record<string, ModelLimits>>;
|
|
19
|
+
/** Number of LiteLLM chat/completion models normalized (before alias keys). */
|
|
20
|
+
readonly model_count: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LitellmFetchDeps {
|
|
24
|
+
readonly fetchFn?: typeof fetch;
|
|
25
|
+
readonly pricingUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class LitellmFetchError extends Error {
|
|
29
|
+
override readonly name = 'LitellmFetchError';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve pricing URL from env with documented LiteLLM GitHub default.
|
|
34
|
+
*/
|
|
35
|
+
export function getLitellmPricingUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
36
|
+
const configured = env.LITELLM_PRICING_URL?.trim();
|
|
37
|
+
return configured && configured.length > 0 ? configured : DEFAULT_LITELLM_PRICING_URL;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Weighted blend of input/output per-token rates → USD per 1M tokens.
|
|
42
|
+
* Matches the registry-cost formula planned for SP-046.
|
|
43
|
+
*/
|
|
44
|
+
export function computeCostPer1MTokens(
|
|
45
|
+
inputCostPerToken: number,
|
|
46
|
+
outputCostPerToken: number,
|
|
47
|
+
): number {
|
|
48
|
+
const inputPer1M = inputCostPerToken * 1_000_000;
|
|
49
|
+
const outputPer1M = outputCostPerToken * 1_000_000;
|
|
50
|
+
return (inputPer1M + outputPer1M) / 2;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readNumberField(entry: Record<string, unknown>, field: string): number | undefined {
|
|
58
|
+
const value = entry[field];
|
|
59
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readStringField(entry: Record<string, unknown>, field: string): string | undefined {
|
|
63
|
+
const value = entry[field];
|
|
64
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readPositiveIntField(entry: Record<string, unknown>, field: string): number | undefined {
|
|
68
|
+
const value = entry[field];
|
|
69
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseModelLimits(entry: Record<string, unknown>): ModelLimits | undefined {
|
|
73
|
+
const maxInputTokens = readPositiveIntField(entry, 'max_input_tokens');
|
|
74
|
+
const maxOutputTokens =
|
|
75
|
+
readPositiveIntField(entry, 'max_output_tokens') ?? readPositiveIntField(entry, 'max_tokens');
|
|
76
|
+
|
|
77
|
+
if (maxInputTokens === undefined && maxOutputTokens === undefined) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...(maxInputTokens !== undefined ? { max_input_tokens: maxInputTokens } : {}),
|
|
83
|
+
...(maxOutputTokens !== undefined ? { max_output_tokens: maxOutputTokens } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function storeLimitsAlias(
|
|
88
|
+
registry_limits_snapshot: Record<string, ModelLimits>,
|
|
89
|
+
key: string,
|
|
90
|
+
limits: ModelLimits,
|
|
91
|
+
): void {
|
|
92
|
+
registry_limits_snapshot[key] = limits;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Normalize LiteLLM pricing JSON into registry_snapshot rates.
|
|
97
|
+
* Validates shape and fails fast with actionable errors.
|
|
98
|
+
*/
|
|
99
|
+
export function normalizeLitellmPricing(raw: unknown): LitellmFetchResult {
|
|
100
|
+
if (!isRecord(raw)) {
|
|
101
|
+
throw new LitellmFetchError(
|
|
102
|
+
'LiteLLM pricing response must be a JSON object. Check LITELLM_PRICING_URL points at model_prices_and_context_window.json.',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const registry_snapshot: Record<string, number> = {};
|
|
107
|
+
const registry_limits_snapshot: Record<string, ModelLimits> = {};
|
|
108
|
+
let modelCount = 0;
|
|
109
|
+
|
|
110
|
+
for (const [modelKey, entry] of Object.entries(raw)) {
|
|
111
|
+
if (modelKey === 'sample_spec') {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isRecord(entry)) {
|
|
116
|
+
throw new LitellmFetchError(
|
|
117
|
+
`LiteLLM pricing entry "${modelKey}" must be an object. Received ${typeof entry}.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const mode = readStringField(entry, 'mode');
|
|
122
|
+
if (mode !== undefined && !CHAT_MODES.has(mode)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const inputCost = readNumberField(entry, 'input_cost_per_token');
|
|
127
|
+
const outputCost = readNumberField(entry, 'output_cost_per_token');
|
|
128
|
+
if (inputCost === undefined || outputCost === undefined) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const costPer1M = computeCostPer1MTokens(inputCost, outputCost);
|
|
133
|
+
registry_snapshot[modelKey] = costPer1M;
|
|
134
|
+
modelCount += 1;
|
|
135
|
+
|
|
136
|
+
const limits = parseModelLimits(entry);
|
|
137
|
+
if (limits !== undefined) {
|
|
138
|
+
storeLimitsAlias(registry_limits_snapshot, modelKey, limits);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const provider = readStringField(entry, 'litellm_provider');
|
|
142
|
+
if (provider !== undefined) {
|
|
143
|
+
registry_snapshot[`${provider}/${modelKey}`] = costPer1M;
|
|
144
|
+
if (limits !== undefined) {
|
|
145
|
+
storeLimitsAlias(registry_limits_snapshot, `${provider}/${modelKey}`, limits);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Object.keys(registry_snapshot).length === 0) {
|
|
151
|
+
throw new LitellmFetchError(
|
|
152
|
+
'LiteLLM pricing response contained no chat models with input/output token costs.',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
registry_snapshot,
|
|
158
|
+
registry_limits_snapshot,
|
|
159
|
+
model_count: modelCount,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface LitellmPriceCatalogResult {
|
|
164
|
+
readonly catalog: PriceCatalog;
|
|
165
|
+
/** Number of chat/completion models in the LiteLLM source payload. */
|
|
166
|
+
readonly model_count: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Fetch LiteLLM pricing and build a PriceCatalog payload (does not persist).
|
|
171
|
+
*/
|
|
172
|
+
export async function fetchLitellmPriceCatalog(
|
|
173
|
+
deps: LitellmFetchDeps = {},
|
|
174
|
+
): Promise<LitellmPriceCatalogResult> {
|
|
175
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
176
|
+
const url = deps.pricingUrl ?? getLitellmPricingUrl();
|
|
177
|
+
|
|
178
|
+
let response: Response;
|
|
179
|
+
try {
|
|
180
|
+
response = await fetchFn(url);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
183
|
+
throw new LitellmFetchError(
|
|
184
|
+
`Failed to fetch LiteLLM pricing from ${url}: ${detail}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
throw new LitellmFetchError(
|
|
190
|
+
`LiteLLM pricing fetch failed (${response.status} ${response.statusText}) from ${url}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let raw: unknown;
|
|
195
|
+
try {
|
|
196
|
+
raw = await response.json();
|
|
197
|
+
} catch {
|
|
198
|
+
throw new LitellmFetchError(
|
|
199
|
+
`LiteLLM pricing response from ${url} is not valid JSON.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const normalized = normalizeLitellmPricing(raw);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
catalog: {
|
|
207
|
+
registry_snapshot: normalized.registry_snapshot,
|
|
208
|
+
registry_limits_snapshot: normalized.registry_limits_snapshot,
|
|
209
|
+
user_overrides: {},
|
|
210
|
+
last_updated: new Date().toISOString(),
|
|
211
|
+
source: 'registry',
|
|
212
|
+
},
|
|
213
|
+
model_count: normalized.model_count,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tri-tier price broker — FR-019 (T053).
|
|
3
|
+
*
|
|
4
|
+
* Resolves per-model pricing using a strict priority cascade:
|
|
5
|
+
* 1. Operator overrides (highest priority)
|
|
6
|
+
* 2. Registry snapshot (refreshed external data)
|
|
7
|
+
* 3. YAML/catalog fallback (ModelProfile.pricing.fallback_cost_per_1m)
|
|
8
|
+
*
|
|
9
|
+
* The broker is a pure function layer; it reads from the PriceCatalog
|
|
10
|
+
* and ModelProfile without side effects.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ModelProfile, ModelLimits, PriceCatalog, PriceSource } from '../../domain/types/index.js';
|
|
14
|
+
import { getDefaultLimitsForTier } from '../../config/pi-model-mapper.js';
|
|
15
|
+
|
|
16
|
+
export interface ResolvedPrice {
|
|
17
|
+
readonly model_id: string;
|
|
18
|
+
readonly cost_per_1m_tokens: number;
|
|
19
|
+
readonly source: PriceSource;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResolvedLimits {
|
|
23
|
+
readonly model_id: string;
|
|
24
|
+
readonly limits: ModelLimits;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function lookupRegistryLimits(
|
|
28
|
+
model: ModelProfile,
|
|
29
|
+
catalog: PriceCatalog,
|
|
30
|
+
): ModelLimits | undefined {
|
|
31
|
+
const snapshot = catalog.registry_limits_snapshot;
|
|
32
|
+
if (!snapshot) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const registryKey = model.pricing.registry_key ?? model.id;
|
|
37
|
+
return snapshot[registryKey] ?? snapshot[model.id];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve effective context limits for a model.
|
|
42
|
+
*
|
|
43
|
+
* Priority: YAML/profile override → LiteLLM registry snapshot → tier default.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveLimits(
|
|
46
|
+
model: ModelProfile,
|
|
47
|
+
catalog: PriceCatalog | null,
|
|
48
|
+
): ResolvedLimits {
|
|
49
|
+
const yamlLimits = model.limits;
|
|
50
|
+
const registryLimits = catalog ? lookupRegistryLimits(model, catalog) : undefined;
|
|
51
|
+
const defaults = getDefaultLimitsForTier(model.tier);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
model_id: model.id,
|
|
55
|
+
limits: {
|
|
56
|
+
max_input_tokens:
|
|
57
|
+
yamlLimits?.max_input_tokens ??
|
|
58
|
+
registryLimits?.max_input_tokens ??
|
|
59
|
+
defaults.max_input_tokens,
|
|
60
|
+
max_output_tokens:
|
|
61
|
+
yamlLimits?.max_output_tokens ??
|
|
62
|
+
registryLimits?.max_output_tokens ??
|
|
63
|
+
defaults.max_output_tokens,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Apply resolved context limits to fleet profiles for routing gates (SP-092).
|
|
70
|
+
*/
|
|
71
|
+
export function applyCatalogLimitsToFleet(
|
|
72
|
+
fleet: readonly ModelProfile[],
|
|
73
|
+
catalog: PriceCatalog | null,
|
|
74
|
+
): ModelProfile[] {
|
|
75
|
+
return fleet.map((profile) => {
|
|
76
|
+
const resolved = resolveLimits(profile, catalog);
|
|
77
|
+
return {
|
|
78
|
+
...profile,
|
|
79
|
+
limits: resolved.limits,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the effective price for a single model.
|
|
86
|
+
*
|
|
87
|
+
* Priority: operator override → registry snapshot → catalog fallback.
|
|
88
|
+
* Always returns a price — the fallback from ModelProfile is guaranteed
|
|
89
|
+
* by schema to exist.
|
|
90
|
+
*/
|
|
91
|
+
export function resolvePrice(
|
|
92
|
+
model: ModelProfile,
|
|
93
|
+
catalog: PriceCatalog | null,
|
|
94
|
+
): ResolvedPrice {
|
|
95
|
+
if (catalog) {
|
|
96
|
+
const override = catalog.user_overrides[model.id];
|
|
97
|
+
if (override !== undefined) {
|
|
98
|
+
return {
|
|
99
|
+
model_id: model.id,
|
|
100
|
+
cost_per_1m_tokens: override,
|
|
101
|
+
source: 'override',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const registryKey = model.pricing.registry_key ?? model.id;
|
|
106
|
+
const registryPrice = catalog.registry_snapshot[registryKey];
|
|
107
|
+
if (registryPrice !== undefined) {
|
|
108
|
+
return {
|
|
109
|
+
model_id: model.id,
|
|
110
|
+
cost_per_1m_tokens: registryPrice,
|
|
111
|
+
source: 'registry',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
model_id: model.id,
|
|
118
|
+
cost_per_1m_tokens: model.pricing.fallback_cost_per_1m,
|
|
119
|
+
source: 'yaml_fallback',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve prices for the entire fleet in a single pass.
|
|
125
|
+
*
|
|
126
|
+
* Returns a Map keyed by model ID for O(1) lookup during scoring.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveFleetPrices(
|
|
129
|
+
fleet: readonly ModelProfile[],
|
|
130
|
+
catalog: PriceCatalog | null,
|
|
131
|
+
): ReadonlyMap<string, ResolvedPrice> {
|
|
132
|
+
const prices = new Map<string, ResolvedPrice>();
|
|
133
|
+
for (const model of fleet) {
|
|
134
|
+
prices.set(model.id, resolvePrice(model, catalog));
|
|
135
|
+
}
|
|
136
|
+
return prices;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply tri-tier resolved prices and context limits to fleet profiles.
|
|
141
|
+
* When no catalog is loaded, applies tier default limits only.
|
|
142
|
+
*/
|
|
143
|
+
export function applyCatalogPricesToFleet(
|
|
144
|
+
fleet: readonly ModelProfile[],
|
|
145
|
+
catalog: PriceCatalog | null,
|
|
146
|
+
): ModelProfile[] {
|
|
147
|
+
if (!catalog) {
|
|
148
|
+
return applyCatalogLimitsToFleet(fleet, catalog);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const prices = resolveFleetPrices(fleet, catalog);
|
|
152
|
+
|
|
153
|
+
const priced = fleet.map((profile) => {
|
|
154
|
+
const resolved = prices.get(profile.id);
|
|
155
|
+
if (!resolved || resolved.source === 'yaml_fallback') {
|
|
156
|
+
return profile;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...profile,
|
|
161
|
+
pricing: {
|
|
162
|
+
...profile.pricing,
|
|
163
|
+
fallback_cost_per_1m: resolved.cost_per_1m_tokens,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return applyCatalogLimitsToFleet(priced, catalog);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve per-request cost rate for frugality scoring and telemetry (SP-096).
|
|
173
|
+
* Subscription-quota virtual cost takes precedence over API/catalog rates.
|
|
174
|
+
*/
|
|
175
|
+
export function resolveFrugalityCostPer1M(
|
|
176
|
+
model: ModelProfile,
|
|
177
|
+
catalog: PriceCatalog | null,
|
|
178
|
+
): number {
|
|
179
|
+
if (model.pricing.quota_cost_per_1m !== undefined) {
|
|
180
|
+
return model.pricing.quota_cost_per_1m;
|
|
181
|
+
}
|
|
182
|
+
return resolvePrice(model, catalog).cost_per_1m_tokens;
|
|
183
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pricing staleness monitor — FR-020 (T054).
|
|
3
|
+
*
|
|
4
|
+
* Checks whether the PriceCatalog's last_updated timestamp exceeds a
|
|
5
|
+
* configurable staleness threshold (default: 14 days). When stale,
|
|
6
|
+
* emits a structured warning for operator visibility.
|
|
7
|
+
*
|
|
8
|
+
* The monitor is stateless and side-effect-free beyond returning a
|
|
9
|
+
* diagnostic result. Callers (gateway, CLI, health-check) decide how
|
|
10
|
+
* to surface the warning.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PriceCatalog } from '../../domain/types/index.js';
|
|
14
|
+
|
|
15
|
+
const MS_PER_DAY = 86_400_000;
|
|
16
|
+
|
|
17
|
+
export interface StalenessResult {
|
|
18
|
+
readonly stale: boolean;
|
|
19
|
+
readonly age_days: number;
|
|
20
|
+
readonly threshold_days: number;
|
|
21
|
+
readonly warning?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Evaluate pricing freshness against the configured threshold.
|
|
26
|
+
*
|
|
27
|
+
* @param catalog Current price catalog, or null if none loaded.
|
|
28
|
+
* @param stalenessDays Operator-configured threshold (default 14).
|
|
29
|
+
* @param now Reference time for testability.
|
|
30
|
+
*/
|
|
31
|
+
export function checkStaleness(
|
|
32
|
+
catalog: PriceCatalog | null,
|
|
33
|
+
stalenessDays: number = 14,
|
|
34
|
+
now: Date = new Date(),
|
|
35
|
+
): StalenessResult {
|
|
36
|
+
if (!catalog) {
|
|
37
|
+
return {
|
|
38
|
+
stale: true,
|
|
39
|
+
age_days: Infinity,
|
|
40
|
+
threshold_days: stalenessDays,
|
|
41
|
+
warning:
|
|
42
|
+
'No pricing catalog loaded. Model costs will use YAML fallback values.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lastUpdated = new Date(catalog.last_updated);
|
|
47
|
+
const ageDays = (now.getTime() - lastUpdated.getTime()) / MS_PER_DAY;
|
|
48
|
+
|
|
49
|
+
if (ageDays > stalenessDays) {
|
|
50
|
+
return {
|
|
51
|
+
stale: true,
|
|
52
|
+
age_days: Math.round(ageDays * 10) / 10,
|
|
53
|
+
threshold_days: stalenessDays,
|
|
54
|
+
warning:
|
|
55
|
+
`Pricing data is ${Math.round(ageDays)} days old (threshold: ${stalenessDays} days). ` +
|
|
56
|
+
'Consider refreshing registry rates to ensure accurate cost estimation.',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
stale: false,
|
|
62
|
+
age_days: Math.round(ageDays * 10) / 10,
|
|
63
|
+
threshold_days: stalenessDays,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retention bounds for routing dataset records (in-memory and SQLite).
|
|
3
|
+
* GitHub #8: 30 days / 10k rows.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RoutingDatasetRecord } from '../../domain/types/index.js';
|
|
7
|
+
|
|
8
|
+
export const DATASET_WINDOW_DAYS = 30;
|
|
9
|
+
export const DATASET_WINDOW_MS = DATASET_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
|
10
|
+
export const DATASET_MAX_ENTRIES = 10_000;
|
|
11
|
+
|
|
12
|
+
/** Remove dataset entries older than the rolling window. */
|
|
13
|
+
export function evictExpiredDatasetEntries(
|
|
14
|
+
entries: RoutingDatasetRecord[],
|
|
15
|
+
windowMs: number = DATASET_WINDOW_MS,
|
|
16
|
+
): void {
|
|
17
|
+
const cutoff = Date.now() - windowMs;
|
|
18
|
+
|
|
19
|
+
while (entries.length > 0) {
|
|
20
|
+
const oldest = entries[0]!;
|
|
21
|
+
if (new Date(oldest.timestamp).getTime() < cutoff) {
|
|
22
|
+
entries.shift();
|
|
23
|
+
} else {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Make room for one more dataset entry before append. */
|
|
30
|
+
export function makeDatasetRoom(
|
|
31
|
+
entries: RoutingDatasetRecord[],
|
|
32
|
+
maxEntries: number = DATASET_MAX_ENTRIES,
|
|
33
|
+
windowMs: number = DATASET_WINDOW_MS,
|
|
34
|
+
): void {
|
|
35
|
+
evictExpiredDatasetEntries(entries, windowMs);
|
|
36
|
+
while (entries.length >= maxEntries) {
|
|
37
|
+
entries.shift();
|
|
38
|
+
}
|
|
39
|
+
}
|