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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type Context,
|
|
5
|
+
type Message,
|
|
6
|
+
type SimpleStreamOptions,
|
|
7
|
+
type TextContent,
|
|
8
|
+
} from '@earendil-works/pi-ai/compat';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
Message as RoutingMessage,
|
|
12
|
+
RoutingRequest,
|
|
13
|
+
TurnType,
|
|
14
|
+
} from '../../../src/domain/types/index.js';
|
|
15
|
+
import { LifecycleHookState } from '../../../src/index.js';
|
|
16
|
+
|
|
17
|
+
const TOKEN_ESTIMATE_KEYS = [
|
|
18
|
+
'estimatedInputTokens',
|
|
19
|
+
'estimated_input_tokens',
|
|
20
|
+
'contextTokens',
|
|
21
|
+
'tokenCount',
|
|
22
|
+
'tokens',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
function readOptionalTokenEstimate(source: unknown): number | undefined {
|
|
26
|
+
if (source === null || typeof source !== 'object') {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const record = source as Record<string, unknown>;
|
|
31
|
+
for (const key of TOKEN_ESTIMATE_KEYS) {
|
|
32
|
+
const value = record[key];
|
|
33
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
|
|
34
|
+
return Math.floor(value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function estimateInputTokens(
|
|
42
|
+
context: Context,
|
|
43
|
+
options?: SimpleStreamOptions,
|
|
44
|
+
): number {
|
|
45
|
+
const fromOptions = readOptionalTokenEstimate(options);
|
|
46
|
+
if (fromOptions !== undefined) {
|
|
47
|
+
return fromOptions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const fromContext = readOptionalTokenEstimate(context);
|
|
51
|
+
if (fromContext !== undefined) {
|
|
52
|
+
return fromContext;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mapped = mapContextMessages(context.messages);
|
|
56
|
+
let charCount = mapped.reduce((sum, message) => sum + message.content.length, 0);
|
|
57
|
+
if (context.systemPrompt) {
|
|
58
|
+
charCount += context.systemPrompt.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (context.messages.length === 0 && !context.systemPrompt) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Math.max(1, Math.ceil(charCount / 4));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function messageContentToString(content: string | readonly (TextContent | { type: string })[]): string {
|
|
69
|
+
if (typeof content === 'string') {
|
|
70
|
+
return content;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return content
|
|
74
|
+
.filter((block): block is TextContent => block.type === 'text')
|
|
75
|
+
.map((block) => block.text)
|
|
76
|
+
.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function extractPromptText(messages: readonly Message[]): string {
|
|
80
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
81
|
+
const message = messages[i];
|
|
82
|
+
if (message?.role === 'user') {
|
|
83
|
+
const text = messageContentToString(message.content);
|
|
84
|
+
if (text.trim()) {
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function deriveTurnType(messages: readonly Message[]): TurnType {
|
|
93
|
+
if (messages.length === 0) {
|
|
94
|
+
return 'unknown';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lastMessage = messages[messages.length - 1];
|
|
98
|
+
if (!lastMessage) {
|
|
99
|
+
return 'unknown';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (lastMessage.role === 'toolResult') {
|
|
103
|
+
return 'tool_result';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (lastMessage.role === 'user') {
|
|
107
|
+
const text = messageContentToString(lastMessage.content).toLowerCase();
|
|
108
|
+
if (
|
|
109
|
+
text.includes('plan') ||
|
|
110
|
+
text.includes('architect') ||
|
|
111
|
+
text.includes('design')
|
|
112
|
+
) {
|
|
113
|
+
return 'planning';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return 'main_loop';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mapContextMessages(messages: readonly Message[]): RoutingMessage[] {
|
|
121
|
+
return messages.map((message) => {
|
|
122
|
+
if (message.role === 'user') {
|
|
123
|
+
return {
|
|
124
|
+
role: message.role,
|
|
125
|
+
content: messageContentToString(message.content),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (message.role === 'assistant') {
|
|
130
|
+
const content = message.content
|
|
131
|
+
.map((block) => {
|
|
132
|
+
if (block.type === 'text') {
|
|
133
|
+
return block.text;
|
|
134
|
+
}
|
|
135
|
+
if (block.type === 'thinking') {
|
|
136
|
+
return block.thinking;
|
|
137
|
+
}
|
|
138
|
+
return '';
|
|
139
|
+
})
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join('\n');
|
|
142
|
+
|
|
143
|
+
return { role: message.role, content };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
role: 'tool',
|
|
148
|
+
content: messageContentToString(message.content),
|
|
149
|
+
tool_blocks: [],
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function buildRoutingRequest(
|
|
155
|
+
context: Context,
|
|
156
|
+
options: SimpleStreamOptions | undefined,
|
|
157
|
+
lifecycleHookState?: LifecycleHookState,
|
|
158
|
+
): RoutingRequest {
|
|
159
|
+
const sessionId = options?.sessionId ?? randomUUID();
|
|
160
|
+
const lifecycleFlags = lifecycleHookState?.consume(sessionId) ?? {};
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
request_id: randomUUID(),
|
|
164
|
+
session_id: sessionId,
|
|
165
|
+
prompt_text: extractPromptText(context.messages),
|
|
166
|
+
messages: mapContextMessages(context.messages),
|
|
167
|
+
turn_type: deriveTurnType(context.messages),
|
|
168
|
+
estimated_input_tokens: estimateInputTokens(context, options),
|
|
169
|
+
...lifecycleFlags,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RoutingDecision, RoutingRequest } from '../../../src/domain/types/index.js';
|
|
2
|
+
import type { SessionRoutingSnapshot } from '../../../src/infrastructure/telemetry/outcome-recorder.js';
|
|
3
|
+
import type { StreamDelegationDeps } from './types.js';
|
|
4
|
+
|
|
5
|
+
export function capturePreRouteOutcomes(
|
|
6
|
+
request: RoutingRequest,
|
|
7
|
+
deps: StreamDelegationDeps,
|
|
8
|
+
priorSnapshot: SessionRoutingSnapshot | undefined,
|
|
9
|
+
hadPin: boolean,
|
|
10
|
+
): void {
|
|
11
|
+
if (!priorSnapshot || !deps.outcomeRecorder) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sessionId = request.session_id;
|
|
16
|
+
|
|
17
|
+
if (request.compaction_flag && hadPin) {
|
|
18
|
+
deps.outcomeRecorder.recordCompactionPinBreak(priorSnapshot, sessionId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (request.force_model_id) {
|
|
22
|
+
deps.outcomeRecorder.recordModelOverride(
|
|
23
|
+
priorSnapshot,
|
|
24
|
+
sessionId,
|
|
25
|
+
request.force_model_id,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function updateSessionRoutingSnapshot(
|
|
31
|
+
deps: StreamDelegationDeps,
|
|
32
|
+
sessionId: string | undefined,
|
|
33
|
+
request: RoutingRequest,
|
|
34
|
+
decision: RoutingDecision,
|
|
35
|
+
): void {
|
|
36
|
+
if (!sessionId || !deps.sessionRouting) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
deps.sessionRouting.set(sessionId, {
|
|
41
|
+
lastRequestId: request.request_id,
|
|
42
|
+
lastSelectedModelId: decision.selected_model_id,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
|
|
3
|
+
import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
|
|
4
|
+
import {
|
|
5
|
+
bindSharedModelRegistry,
|
|
6
|
+
ensureFleetFresh,
|
|
7
|
+
formatLmuStatus,
|
|
8
|
+
rebuildFleet,
|
|
9
|
+
} from './fleet-bootstrap.js';
|
|
10
|
+
import { notifyPricingStalenessIfNeeded } from './pricing-lifecycle.js';
|
|
11
|
+
import type { FleetMode, SmartRouterRuntime } from './types.js';
|
|
12
|
+
|
|
13
|
+
export const FLEET_MODE_ENTRY_TYPE = 'smart-router-fleet-mode' as const;
|
|
14
|
+
|
|
15
|
+
const SMART_ROUTER_PROVIDER = 'smart-router' as const;
|
|
16
|
+
const SMART_ROUTER_AUTO_ID = 'auto' as const;
|
|
17
|
+
|
|
18
|
+
export function isSmartRouterActive(model: { provider: string; id: string }): boolean {
|
|
19
|
+
return model.provider === SMART_ROUTER_PROVIDER && model.id === SMART_ROUTER_AUTO_ID;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseFleetModeEntry(data: unknown): FleetMode | undefined {
|
|
23
|
+
if (
|
|
24
|
+
typeof data === 'object' &&
|
|
25
|
+
data !== null &&
|
|
26
|
+
'mode' in data &&
|
|
27
|
+
(data.mode === 'scoped' || data.mode === 'all')
|
|
28
|
+
) {
|
|
29
|
+
return data.mode;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function restoreFleetModeFromSession(ctx: ExtensionContext): FleetMode | undefined {
|
|
35
|
+
const entries = ctx.sessionManager.getEntries();
|
|
36
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
37
|
+
const entry = entries[i];
|
|
38
|
+
if (entry?.type === 'custom' && entry.customType === FLEET_MODE_ENTRY_TYPE) {
|
|
39
|
+
return parseFleetModeEntry(entry.data);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function restoreLmuFromLedger(runtime: SmartRouterRuntime, sessionId: string): void {
|
|
46
|
+
const lastExec = runtime.executionLedger.getLastExecution(sessionId);
|
|
47
|
+
if (lastExec) {
|
|
48
|
+
runtime.setLmuStatus?.(lastExec.id);
|
|
49
|
+
} else if (runtime.lastDecision) {
|
|
50
|
+
runtime.setLmuStatus?.(runtime.lastDecision.selected_model_id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function wireLmuStatusHandlers(
|
|
55
|
+
runtime: SmartRouterRuntime,
|
|
56
|
+
ctx: ExtensionContext,
|
|
57
|
+
getActiveModel: () => { provider: string; id: string },
|
|
58
|
+
): void {
|
|
59
|
+
runtime.setLmuStatus = (modelId) => {
|
|
60
|
+
if (!isSmartRouterActive(getActiveModel())) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
ctx.ui.setStatus(
|
|
64
|
+
'smart-router-lmu',
|
|
65
|
+
formatLmuStatus(modelId, ctx.ui.theme as { fg: (color: string, text: string) => string }),
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
runtime.clearLmuStatus = () => {
|
|
69
|
+
ctx.ui.setStatus('smart-router-lmu', undefined);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function setupSessionHooks(
|
|
74
|
+
pi: ExtensionAPI,
|
|
75
|
+
runtime: SmartRouterRuntime,
|
|
76
|
+
sessionPinner: SessionPinner,
|
|
77
|
+
datasetNotify: { fn: ((message: string) => void) | undefined },
|
|
78
|
+
): void {
|
|
79
|
+
let activeModel: { provider: string; id: string } | undefined;
|
|
80
|
+
|
|
81
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
82
|
+
activeModel = ctx.model;
|
|
83
|
+
bindSharedModelRegistry(runtime, ctx.modelRegistry);
|
|
84
|
+
runtime.sessionCwd = ctx.cwd;
|
|
85
|
+
runtime.streamDeps.ensureFleetFresh = async () => {
|
|
86
|
+
if (runtime.sessionCwd === undefined) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await ensureFleetFresh(runtime, pi, runtime.sessionCwd);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const restoredMode = restoreFleetModeFromSession(ctx);
|
|
93
|
+
if (restoredMode) {
|
|
94
|
+
runtime.fleetMode = restoredMode;
|
|
95
|
+
}
|
|
96
|
+
await rebuildFleet(runtime, pi, ctx.cwd);
|
|
97
|
+
|
|
98
|
+
notifyPricingStalenessIfNeeded(runtime, (message, level) => {
|
|
99
|
+
ctx.ui.notify(message, level);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
wireLmuStatusHandlers(runtime, ctx, () => {
|
|
103
|
+
const model = activeModel ?? ctx.model;
|
|
104
|
+
return model ?? { provider: '', id: '' };
|
|
105
|
+
});
|
|
106
|
+
runtime.notifyDatasetEnabled = (message) => {
|
|
107
|
+
ctx.ui.notify(message, 'info');
|
|
108
|
+
};
|
|
109
|
+
datasetNotify.fn = runtime.notifyDatasetEnabled;
|
|
110
|
+
|
|
111
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
112
|
+
await sessionPinner.restoreSessionPin(sessionId);
|
|
113
|
+
|
|
114
|
+
if (ctx.model !== undefined && isSmartRouterActive(ctx.model)) {
|
|
115
|
+
restoreLmuFromLedger(runtime, sessionId);
|
|
116
|
+
} else {
|
|
117
|
+
runtime.clearLmuStatus?.();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
pi.on('model_select', (event, ctx) => {
|
|
122
|
+
activeModel = event.model;
|
|
123
|
+
if (isSmartRouterActive(event.model)) {
|
|
124
|
+
restoreLmuFromLedger(runtime, ctx.sessionManager.getSessionId());
|
|
125
|
+
} else {
|
|
126
|
+
runtime.clearLmuStatus?.();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
pi.on('session_shutdown', async (_event, ctx) => {
|
|
131
|
+
activeModel = undefined;
|
|
132
|
+
delete runtime.setLmuStatus;
|
|
133
|
+
delete runtime.clearLmuStatus;
|
|
134
|
+
delete runtime.notifyDatasetEnabled;
|
|
135
|
+
delete runtime.sessionCwd;
|
|
136
|
+
delete runtime.streamDeps.ensureFleetFresh;
|
|
137
|
+
datasetNotify.fn = undefined;
|
|
138
|
+
ctx.ui.setStatus('smart-router-lmu', undefined);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Api,
|
|
3
|
+
type AssistantMessageEventStream,
|
|
4
|
+
type Context,
|
|
5
|
+
type Model,
|
|
6
|
+
type SimpleStreamOptions,
|
|
7
|
+
createAssistantMessageEventStream,
|
|
8
|
+
} from '@earendil-works/pi-ai/compat';
|
|
9
|
+
|
|
10
|
+
import type { RoutingDecision, RoutingFeatureSidecar } from '../../../src/domain/types/index.js';
|
|
11
|
+
import { createErrorMessage } from './delegation-runtime.js';
|
|
12
|
+
import { routeAndDelegate } from './route-and-delegate.js';
|
|
13
|
+
import type { StreamDelegationDeps } from './types.js';
|
|
14
|
+
|
|
15
|
+
function isRoutingLogEnabled(): boolean {
|
|
16
|
+
return process.env.SMART_ROUTER_LOG_ROUTING === '1';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function logRoutingDecision(
|
|
20
|
+
decision: RoutingDecision,
|
|
21
|
+
delegate?: { provider: string; modelId: string; api: Api },
|
|
22
|
+
): void {
|
|
23
|
+
if (!isRoutingLogEnabled()) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.warn(
|
|
28
|
+
'[smart-router] routing decision',
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
request_id: decision.request_id,
|
|
31
|
+
selected_model_id: decision.selected_model_id,
|
|
32
|
+
tier: decision.tier,
|
|
33
|
+
stage: decision.stage,
|
|
34
|
+
reason_code: decision.reason_code,
|
|
35
|
+
routing_latency_ms: decision.routing_latency_ms,
|
|
36
|
+
features: decision.features ?? null,
|
|
37
|
+
delegate,
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Read dataset feature sidecar from a routing decision (SP-057). */
|
|
43
|
+
export function getRoutingFeatureSidecar(
|
|
44
|
+
decision: RoutingDecision,
|
|
45
|
+
): RoutingFeatureSidecar | undefined {
|
|
46
|
+
return decision.features;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createStreamSimple(deps: StreamDelegationDeps) {
|
|
50
|
+
return function streamSimple(
|
|
51
|
+
model: Model<Api>,
|
|
52
|
+
context: Context,
|
|
53
|
+
options?: SimpleStreamOptions,
|
|
54
|
+
): AssistantMessageEventStream {
|
|
55
|
+
const stream = createAssistantMessageEventStream();
|
|
56
|
+
|
|
57
|
+
void (async () => {
|
|
58
|
+
try {
|
|
59
|
+
await routeAndDelegate(context, options, deps, stream);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
stream.push({
|
|
62
|
+
type: 'error',
|
|
63
|
+
reason: options?.signal?.aborted ? 'aborted' : 'error',
|
|
64
|
+
error: createErrorMessage(model, options, error),
|
|
65
|
+
});
|
|
66
|
+
stream.end();
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
|
|
70
|
+
return stream;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { resolveDelegationOptions, buildDelegationContext } from './delegation-runtime.js';
|
|
75
|
+
export { resolveTargetModel } from './route-and-delegate.js';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Api,
|
|
3
|
+
AssistantMessageEventStream,
|
|
4
|
+
Context,
|
|
5
|
+
Model,
|
|
6
|
+
SimpleStreamOptions,
|
|
7
|
+
} from '@earendil-works/pi-ai/compat';
|
|
8
|
+
import type { ModelRegistry } from '@earendil-works/pi-coding-agent';
|
|
9
|
+
|
|
10
|
+
import type { HydraMatcher } from '../../../src/domain/matching/hydra-matcher.js';
|
|
11
|
+
import { ExecutionLedger } from '../../../src/domain/delegation/execution-ledger.js';
|
|
12
|
+
import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
|
|
13
|
+
import type {
|
|
14
|
+
ModelProfile,
|
|
15
|
+
PriceCatalog,
|
|
16
|
+
RoutingDecision,
|
|
17
|
+
} from '../../../src/domain/types/index.js';
|
|
18
|
+
import type { StorePort } from '../../../src/domain/types/store-port.js';
|
|
19
|
+
import {
|
|
20
|
+
DatasetRecorder,
|
|
21
|
+
} from '../../../src/infrastructure/telemetry/dataset-recorder.js';
|
|
22
|
+
import {
|
|
23
|
+
OutcomeRecorder,
|
|
24
|
+
type SessionRoutingSnapshot,
|
|
25
|
+
} from '../../../src/infrastructure/telemetry/outcome-recorder.js';
|
|
26
|
+
import {
|
|
27
|
+
LifecycleHookState,
|
|
28
|
+
type RouterHandle,
|
|
29
|
+
} from '../../../src/index.js';
|
|
30
|
+
|
|
31
|
+
export type FleetMode = 'scoped' | 'all';
|
|
32
|
+
|
|
33
|
+
export type SmartRouterCommand =
|
|
34
|
+
| { command: 'status' }
|
|
35
|
+
| { command: 'history'; limit: number }
|
|
36
|
+
| { command: 'mode'; mode: FleetMode }
|
|
37
|
+
| { command: 'pricing'; subcommand: 'refresh' }
|
|
38
|
+
| { command: 'export'; subcommand: 'dataset'; limit: number }
|
|
39
|
+
| { command: 'export'; subcommand: 'telemetry-contrib'; limit: number }
|
|
40
|
+
| { command: 'feedback'; rating: 'good' | 'bad' }
|
|
41
|
+
| { command: 'unpin' };
|
|
42
|
+
|
|
43
|
+
/** Provider stream delegate; defaults to pi-ai streamSimple when omitted. */
|
|
44
|
+
export type DelegateStreamFn = (
|
|
45
|
+
model: Model<Api>,
|
|
46
|
+
context: Context,
|
|
47
|
+
options?: SimpleStreamOptions,
|
|
48
|
+
) => AssistantMessageEventStream;
|
|
49
|
+
|
|
50
|
+
export interface StreamDelegationDeps {
|
|
51
|
+
router: RouterHandle;
|
|
52
|
+
modelRegistry: ModelRegistry;
|
|
53
|
+
fleet: ModelProfile[];
|
|
54
|
+
/** Cheap scope fingerprint check before each routed turn. */
|
|
55
|
+
ensureFleetFresh?: () => Promise<void>;
|
|
56
|
+
readonly executionLedger: ExecutionLedger;
|
|
57
|
+
/** Injectable for tests; production uses pi-ai streamSimple. */
|
|
58
|
+
delegateStream?: DelegateStreamFn;
|
|
59
|
+
readonly lifecycleHookState?: LifecycleHookState;
|
|
60
|
+
readonly datasetRecorder?: DatasetRecorder;
|
|
61
|
+
readonly outcomeRecorder?: OutcomeRecorder;
|
|
62
|
+
readonly sessionPinner?: SessionPinner;
|
|
63
|
+
readonly sessionRouting?: Map<string, SessionRoutingSnapshot>;
|
|
64
|
+
onRoutingDecision?: (decision: RoutingDecision) => void;
|
|
65
|
+
/** Fired when a delegated provider stream completes successfully. */
|
|
66
|
+
onDelegatedModel?: (model: { readonly provider: string; readonly id: string }) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SmartRouterRuntime {
|
|
70
|
+
fleetMode: FleetMode;
|
|
71
|
+
lastDecision: RoutingDecision | undefined;
|
|
72
|
+
priceCatalog: PriceCatalog | null;
|
|
73
|
+
/** Cached scope fingerprint; rebuild when this changes. */
|
|
74
|
+
fleetScopeFingerprint?: string;
|
|
75
|
+
/** Session cwd for ensureFleetFresh before routed turns. */
|
|
76
|
+
sessionCwd?: string;
|
|
77
|
+
modelRegistry: ModelRegistry;
|
|
78
|
+
readonly store: StorePort;
|
|
79
|
+
readonly sessionPinner: SessionPinner;
|
|
80
|
+
readonly executionLedger: ExecutionLedger;
|
|
81
|
+
readonly lifecycleHookState: LifecycleHookState;
|
|
82
|
+
readonly datasetRecorder?: DatasetRecorder;
|
|
83
|
+
readonly outcomeRecorder?: OutcomeRecorder;
|
|
84
|
+
readonly sessionRouting: Map<string, SessionRoutingSnapshot>;
|
|
85
|
+
streamDeps: StreamDelegationDeps;
|
|
86
|
+
hydraMatcher: HydraMatcher | undefined;
|
|
87
|
+
setLmuStatus?: (modelId: string) => void;
|
|
88
|
+
clearLmuStatus?: () => void;
|
|
89
|
+
notifyDatasetEnabled?: (message: string) => void;
|
|
90
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createResilientStore,
|
|
5
|
+
SqliteStore,
|
|
6
|
+
SqliteStoreError,
|
|
7
|
+
} from '../../../src/infrastructure/persistence/sqlite-store.js';
|
|
8
|
+
import type { StorePort } from '../../../src/domain/types/store-port.js';
|
|
9
|
+
import type { RateLimitPort } from '../../../src/infrastructure/gateway/gateway-dispatch.js';
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_ROUTER_STATE_DB_PATH = '.pi-smart-router/state.db';
|
|
12
|
+
export const DEFAULT_RATE_LIMIT_MAX_TOKENS = 60;
|
|
13
|
+
export const DEFAULT_RATE_LIMIT_REFILL_RATE = 1;
|
|
14
|
+
|
|
15
|
+
export function getRouterStateDbPath(cwd: string): string {
|
|
16
|
+
const configured = process.env.ROUTER_STATE_DB_PATH?.trim();
|
|
17
|
+
if (configured && configured.length > 0) {
|
|
18
|
+
return configured;
|
|
19
|
+
}
|
|
20
|
+
return join(cwd, DEFAULT_ROUTER_STATE_DB_PATH);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createExtensionStore(cwd: string): StorePort {
|
|
24
|
+
return createResilientStore({
|
|
25
|
+
dbPath: getRouterStateDbPath(cwd),
|
|
26
|
+
models: [],
|
|
27
|
+
}).store;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createSqliteRateLimiter(sqliteStore: SqliteStore): RateLimitPort {
|
|
31
|
+
return {
|
|
32
|
+
consumeToken(key: string, cost = 1) {
|
|
33
|
+
try {
|
|
34
|
+
return sqliteStore.consumeToken(key, cost);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (
|
|
37
|
+
error instanceof SqliteStoreError &&
|
|
38
|
+
error.message.includes('Token bucket not found')
|
|
39
|
+
) {
|
|
40
|
+
sqliteStore.initBucket(
|
|
41
|
+
key,
|
|
42
|
+
DEFAULT_RATE_LIMIT_MAX_TOKENS,
|
|
43
|
+
DEFAULT_RATE_LIMIT_REFILL_RATE,
|
|
44
|
+
);
|
|
45
|
+
return sqliteStore.consumeToken(key, cost);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveRateLimiter(store: StorePort): RateLimitPort | undefined {
|
|
54
|
+
if (!(store instanceof SqliteStore)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return createSqliteRateLimiter(store);
|
|
58
|
+
}
|