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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy-safe routing dataset recorder (SP-058).
|
|
3
|
+
*
|
|
4
|
+
* Maps routing decisions and feature sidecars to RoutingDatasetRecord when
|
|
5
|
+
* SMART_ROUTER_DATASET=1. Never stores prompt text, messages, or tool arguments.
|
|
6
|
+
*
|
|
7
|
+
* Optional install-local prompt fingerprints (SP-061) when
|
|
8
|
+
* SMART_ROUTER_DATASET_FINGERPRINT=1.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHmac, randomBytes } from 'node:crypto';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
RoutingDatasetRecord,
|
|
17
|
+
RoutingDecision,
|
|
18
|
+
RoutingRequest,
|
|
19
|
+
} from '../../domain/types/index.js';
|
|
20
|
+
import type { TierFeatureDatasetScalars } from '../../domain/routing/tier-features.js';
|
|
21
|
+
import {
|
|
22
|
+
buildContextFitObservability,
|
|
23
|
+
buildTierSelectionObservability,
|
|
24
|
+
type ContextFitObservabilityInput,
|
|
25
|
+
} from './routing-telemetry.js';
|
|
26
|
+
|
|
27
|
+
export const DATASET_ENABLED_NOTIFY_MESSAGE =
|
|
28
|
+
'Smart Router dataset mode is enabled. Recording routing metadata and feature fields only — prompt text, messages, and tool arguments are never stored.';
|
|
29
|
+
|
|
30
|
+
export const DATASET_STATE_DIR = '.pi-smart-router';
|
|
31
|
+
export const DATASET_PEPPER_FILENAME = '.dataset-key';
|
|
32
|
+
|
|
33
|
+
const DATASET_PEPPER_BYTES = 32;
|
|
34
|
+
|
|
35
|
+
export function isDatasetRecordingEnabled(): boolean {
|
|
36
|
+
return process.env.SMART_ROUTER_DATASET === '1';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isDatasetFingerprintEnabled(): boolean {
|
|
40
|
+
return (
|
|
41
|
+
isDatasetRecordingEnabled() &&
|
|
42
|
+
process.env.SMART_ROUTER_DATASET_FINGERPRINT === '1'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getDatasetPepperPath(cwd: string = process.cwd()): string {
|
|
47
|
+
return join(cwd, DATASET_STATE_DIR, DATASET_PEPPER_FILENAME);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Load or create the install-local dataset pepper (never exported). */
|
|
51
|
+
export function loadOrCreateDatasetPepper(cwd: string = process.cwd()): Buffer {
|
|
52
|
+
const pepperPath = getDatasetPepperPath(cwd);
|
|
53
|
+
mkdirSync(dirname(pepperPath), { recursive: true });
|
|
54
|
+
|
|
55
|
+
if (existsSync(pepperPath)) {
|
|
56
|
+
const raw = readFileSync(pepperPath, 'utf8').trim();
|
|
57
|
+
return Buffer.from(raw, 'hex');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const pepper = randomBytes(DATASET_PEPPER_BYTES);
|
|
61
|
+
writeFileSync(pepperPath, pepper.toString('hex'), { mode: 0o600 });
|
|
62
|
+
return pepper;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Collapse whitespace and trim for stable duplicate detection. */
|
|
66
|
+
export function normalizePromptForFingerprint(prompt: string): string {
|
|
67
|
+
return prompt.trim().replace(/\s+/g, ' ');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function computePromptFingerprint(pepper: Buffer, prompt: string): string {
|
|
71
|
+
return createHmac('sha256', pepper)
|
|
72
|
+
.update(normalizePromptForFingerprint(prompt))
|
|
73
|
+
.digest('hex');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DatasetRecorderOptions {
|
|
77
|
+
readonly clock?: () => string;
|
|
78
|
+
readonly onRecord?: (record: RoutingDatasetRecord) => void;
|
|
79
|
+
readonly onFirstEnable?: () => void;
|
|
80
|
+
readonly cwd?: string;
|
|
81
|
+
readonly loadPepper?: (cwd: string) => Buffer;
|
|
82
|
+
readonly contextFitInput?: Omit<ContextFitObservabilityInput, 'request' | 'decision'>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hasToolContext(request: RoutingRequest): boolean {
|
|
86
|
+
if (request.turn_type === 'tool_result') {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return request.messages?.some((message) => message.role === 'tool') ?? false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function serializeCandidates(decision: RoutingDecision): string | null {
|
|
94
|
+
const candidates = decision.features?.candidates ?? decision.candidates;
|
|
95
|
+
if (!candidates || candidates.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return JSON.stringify(candidates);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Map a completed routing decision to a privacy-safe dataset row. */
|
|
103
|
+
export function buildDatasetRecord(
|
|
104
|
+
request: RoutingRequest,
|
|
105
|
+
decision: RoutingDecision,
|
|
106
|
+
timestamp: string,
|
|
107
|
+
promptFingerprint: string | null = null,
|
|
108
|
+
tierScalars?: TierFeatureDatasetScalars,
|
|
109
|
+
contextFitInput?: Omit<ContextFitObservabilityInput, 'request' | 'decision'>,
|
|
110
|
+
): RoutingDatasetRecord {
|
|
111
|
+
const triage = decision.features?.triage ?? null;
|
|
112
|
+
const requirements = decision.features?.requirements ?? null;
|
|
113
|
+
const contextFit = buildContextFitObservability({
|
|
114
|
+
request,
|
|
115
|
+
decision,
|
|
116
|
+
...contextFitInput,
|
|
117
|
+
});
|
|
118
|
+
const tierSelection = buildTierSelectionObservability({ decision });
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
request_id: decision.request_id,
|
|
122
|
+
timestamp,
|
|
123
|
+
turn_type: request.turn_type ?? 'unknown',
|
|
124
|
+
stage: decision.stage,
|
|
125
|
+
reason_code: decision.reason_code,
|
|
126
|
+
selected_model_id: decision.selected_model_id,
|
|
127
|
+
tier: decision.tier,
|
|
128
|
+
candidates_json: serializeCandidates(decision),
|
|
129
|
+
prompt_length_chars: request.prompt_text.length,
|
|
130
|
+
estimated_input_tokens: request.estimated_input_tokens ?? null,
|
|
131
|
+
message_count: request.messages?.length ?? 0,
|
|
132
|
+
has_tool_context: hasToolContext(request),
|
|
133
|
+
compaction_flag: request.compaction_flag ?? false,
|
|
134
|
+
triage_verdict: triage?.verdict ?? null,
|
|
135
|
+
triage_reason_code: triage?.reason_code ?? null,
|
|
136
|
+
triage_cyclomatic_score: triage?.cyclomatic_score ?? null,
|
|
137
|
+
triage_trivial_hits: tierScalars?.triage_trivial_hits ?? null,
|
|
138
|
+
triage_complex_hits: tierScalars?.triage_complex_hits ?? null,
|
|
139
|
+
triage_sanitized_length_delta: tierScalars?.triage_sanitized_length_delta ?? null,
|
|
140
|
+
requirement_reasoning: requirements?.reasoning ?? null,
|
|
141
|
+
requirement_code_gen: requirements?.code_gen ?? null,
|
|
142
|
+
requirement_tool_use: requirements?.tool_use ?? null,
|
|
143
|
+
routing_latency_ms: decision.routing_latency_ms,
|
|
144
|
+
estimated_cost_usd: decision.estimated_cost_usd ?? null,
|
|
145
|
+
prompt_fingerprint: promptFingerprint,
|
|
146
|
+
estimated_input_tokens_gate: contextFit?.estimated_input_tokens ?? null,
|
|
147
|
+
context_fit_viable_count: contextFit?.context_fit_viable_count ?? null,
|
|
148
|
+
context_fit_rejected_json: contextFit?.context_fit_rejected_json ?? null,
|
|
149
|
+
context_overflow_pin_break: contextFit?.context_overflow_pin_break ?? false,
|
|
150
|
+
selected_model_max_input_tokens: contextFit?.selected_model_max_input_tokens ?? null,
|
|
151
|
+
context_fit_reason_code: contextFit?.context_fit_reason_code ?? null,
|
|
152
|
+
cluster_id: tierSelection?.cluster_id ?? null,
|
|
153
|
+
cluster_similarity: tierSelection?.cluster_similarity ?? null,
|
|
154
|
+
cluster_margin: tierSelection?.cluster_margin ?? null,
|
|
155
|
+
low_intensity_score: tierSelection?.low_intensity_score ?? null,
|
|
156
|
+
tier_hint: tierSelection?.tier_hint ?? null,
|
|
157
|
+
p_success_cheap: tierSelection?.p_success_cheap ?? null,
|
|
158
|
+
local_eligible_reason: tierSelection?.local_eligible_reason ?? null,
|
|
159
|
+
tier_selection_reason_code: tierSelection?.tier_selection_reason_code ?? null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export class DatasetRecorder {
|
|
164
|
+
private readonly clock: () => string;
|
|
165
|
+
private readonly onRecord: ((record: RoutingDatasetRecord) => void) | undefined;
|
|
166
|
+
private readonly onFirstEnable: (() => void) | undefined;
|
|
167
|
+
private readonly cwd: string;
|
|
168
|
+
private readonly loadPepper: (cwd: string) => Buffer;
|
|
169
|
+
private readonly contextFitInput: Omit<ContextFitObservabilityInput, 'request' | 'decision'> | undefined;
|
|
170
|
+
private enabledNotified = false;
|
|
171
|
+
private pepper: Buffer | null = null;
|
|
172
|
+
|
|
173
|
+
constructor(options?: DatasetRecorderOptions) {
|
|
174
|
+
this.clock = options?.clock ?? (() => new Date().toISOString());
|
|
175
|
+
this.onRecord = options?.onRecord;
|
|
176
|
+
this.onFirstEnable = options?.onFirstEnable;
|
|
177
|
+
this.cwd = options?.cwd ?? process.cwd();
|
|
178
|
+
this.loadPepper = options?.loadPepper ?? loadOrCreateDatasetPepper;
|
|
179
|
+
this.contextFitInput = options?.contextFitInput;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private getPepper(): Buffer {
|
|
183
|
+
if (!this.pepper) {
|
|
184
|
+
this.pepper = this.loadPepper(this.cwd);
|
|
185
|
+
}
|
|
186
|
+
return this.pepper;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Record a routing decision when dataset mode is enabled; no-op when off. */
|
|
190
|
+
record(request: RoutingRequest, decision: RoutingDecision): RoutingDatasetRecord | null {
|
|
191
|
+
if (!isDatasetRecordingEnabled()) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!this.enabledNotified) {
|
|
196
|
+
this.enabledNotified = true;
|
|
197
|
+
this.onFirstEnable?.();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const promptFingerprint = isDatasetFingerprintEnabled()
|
|
201
|
+
? computePromptFingerprint(this.getPepper(), request.prompt_text)
|
|
202
|
+
: null;
|
|
203
|
+
|
|
204
|
+
const record = buildDatasetRecord(
|
|
205
|
+
request,
|
|
206
|
+
decision,
|
|
207
|
+
this.clock(),
|
|
208
|
+
promptFingerprint,
|
|
209
|
+
undefined,
|
|
210
|
+
this.contextFitInput,
|
|
211
|
+
);
|
|
212
|
+
this.onRecord?.(record);
|
|
213
|
+
return record;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retention bounds for routing outcome labels (in-memory and SQLite).
|
|
3
|
+
* Aligns with dataset retention (30 days / 10k rows).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RoutingOutcomeRecord } from '../../domain/types/index.js';
|
|
7
|
+
import {
|
|
8
|
+
DATASET_MAX_ENTRIES,
|
|
9
|
+
DATASET_WINDOW_MS,
|
|
10
|
+
} from './dataset-limits.js';
|
|
11
|
+
|
|
12
|
+
export const OUTCOME_MAX_ENTRIES = DATASET_MAX_ENTRIES;
|
|
13
|
+
export const OUTCOME_WINDOW_MS = DATASET_WINDOW_MS;
|
|
14
|
+
|
|
15
|
+
/** Remove outcome entries older than the rolling window. */
|
|
16
|
+
export function evictExpiredOutcomeEntries(
|
|
17
|
+
entries: RoutingOutcomeRecord[],
|
|
18
|
+
windowMs: number = OUTCOME_WINDOW_MS,
|
|
19
|
+
): void {
|
|
20
|
+
const cutoff = Date.now() - windowMs;
|
|
21
|
+
|
|
22
|
+
while (entries.length > 0) {
|
|
23
|
+
const oldest = entries[0]!;
|
|
24
|
+
if (new Date(oldest.timestamp).getTime() < cutoff) {
|
|
25
|
+
entries.shift();
|
|
26
|
+
} else {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Make room for one more outcome entry before append. */
|
|
33
|
+
export function makeOutcomeRoom(
|
|
34
|
+
entries: RoutingOutcomeRecord[],
|
|
35
|
+
maxEntries: number = OUTCOME_MAX_ENTRIES,
|
|
36
|
+
windowMs: number = OUTCOME_WINDOW_MS,
|
|
37
|
+
): void {
|
|
38
|
+
evictExpiredOutcomeEntries(entries, windowMs);
|
|
39
|
+
while (entries.length >= maxEntries) {
|
|
40
|
+
entries.shift();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavioral outcome label recorder (SP-062).
|
|
3
|
+
*
|
|
4
|
+
* Captures model override, compaction pin break, and user feedback when
|
|
5
|
+
* SMART_ROUTER_DATASET=1. Never stores prompt text.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
OutcomeSignalType,
|
|
10
|
+
RoutingOutcomeRecord,
|
|
11
|
+
} from '../../domain/types/index.js';
|
|
12
|
+
import { isDatasetRecordingEnabled } from './dataset-recorder.js';
|
|
13
|
+
|
|
14
|
+
export interface OutcomeRecorderOptions {
|
|
15
|
+
readonly clock?: () => string;
|
|
16
|
+
readonly onRecord?: (record: RoutingOutcomeRecord) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SessionRoutingSnapshot {
|
|
20
|
+
readonly lastRequestId: string;
|
|
21
|
+
readonly lastSelectedModelId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildOutcomeRecord(
|
|
25
|
+
requestId: string,
|
|
26
|
+
sessionId: string,
|
|
27
|
+
signalType: OutcomeSignalType,
|
|
28
|
+
timestamp: string,
|
|
29
|
+
options?: {
|
|
30
|
+
readonly routedModelId?: string | null;
|
|
31
|
+
readonly overrideModelId?: string | null;
|
|
32
|
+
},
|
|
33
|
+
): RoutingOutcomeRecord {
|
|
34
|
+
return {
|
|
35
|
+
request_id: requestId,
|
|
36
|
+
session_id: sessionId,
|
|
37
|
+
timestamp,
|
|
38
|
+
signal_type: signalType,
|
|
39
|
+
routed_model_id: options?.routedModelId ?? null,
|
|
40
|
+
override_model_id: options?.overrideModelId ?? null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class OutcomeRecorder {
|
|
45
|
+
private readonly clock: () => string;
|
|
46
|
+
private readonly onRecord: ((record: RoutingOutcomeRecord) => void) | undefined;
|
|
47
|
+
|
|
48
|
+
constructor(options?: OutcomeRecorderOptions) {
|
|
49
|
+
this.clock = options?.clock ?? (() => new Date().toISOString());
|
|
50
|
+
this.onRecord = options?.onRecord;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private recordIfEnabled(record: RoutingOutcomeRecord): RoutingOutcomeRecord | null {
|
|
54
|
+
if (!isDatasetRecordingEnabled()) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.onRecord?.(record);
|
|
59
|
+
return record;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
recordModelOverride(
|
|
63
|
+
snapshot: SessionRoutingSnapshot,
|
|
64
|
+
sessionId: string,
|
|
65
|
+
overrideModelId: string,
|
|
66
|
+
): RoutingOutcomeRecord | null {
|
|
67
|
+
if (overrideModelId === snapshot.lastSelectedModelId) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return this.recordIfEnabled(
|
|
72
|
+
buildOutcomeRecord(
|
|
73
|
+
snapshot.lastRequestId,
|
|
74
|
+
sessionId,
|
|
75
|
+
'model_override',
|
|
76
|
+
this.clock(),
|
|
77
|
+
{
|
|
78
|
+
routedModelId: snapshot.lastSelectedModelId,
|
|
79
|
+
overrideModelId,
|
|
80
|
+
},
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
recordCompactionPinBreak(
|
|
86
|
+
snapshot: SessionRoutingSnapshot,
|
|
87
|
+
sessionId: string,
|
|
88
|
+
): RoutingOutcomeRecord | null {
|
|
89
|
+
return this.recordIfEnabled(
|
|
90
|
+
buildOutcomeRecord(
|
|
91
|
+
snapshot.lastRequestId,
|
|
92
|
+
sessionId,
|
|
93
|
+
'compaction_pin_break',
|
|
94
|
+
this.clock(),
|
|
95
|
+
{ routedModelId: snapshot.lastSelectedModelId },
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
recordFeedback(
|
|
101
|
+
snapshot: SessionRoutingSnapshot,
|
|
102
|
+
sessionId: string,
|
|
103
|
+
rating: 'good' | 'bad',
|
|
104
|
+
): RoutingOutcomeRecord | null {
|
|
105
|
+
const signalType: OutcomeSignalType =
|
|
106
|
+
rating === 'good' ? 'feedback_good' : 'feedback_bad';
|
|
107
|
+
|
|
108
|
+
return this.recordIfEnabled(
|
|
109
|
+
buildOutcomeRecord(
|
|
110
|
+
snapshot.lastRequestId,
|
|
111
|
+
sessionId,
|
|
112
|
+
signalType,
|
|
113
|
+
this.clock(),
|
|
114
|
+
{ routedModelId: snapshot.lastSelectedModelId },
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|