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,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_HISTORY_LIMIT,
|
|
3
|
+
MAX_HISTORY_LIMIT,
|
|
4
|
+
} from '../../../src/infrastructure/telemetry/telemetry-limits.js';
|
|
5
|
+
import type { RoutingDecision, RoutingTelemetry } from '../../../src/domain/types/index.js';
|
|
6
|
+
import { SMART_ROUTER_USAGE } from './commands.js';
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_TELEMETRY_CONTRIB_EXPORT_LIMIT,
|
|
9
|
+
parseExportTelemetryContribArgs,
|
|
10
|
+
} from '../../../src/cli/smart-router-cli.js';
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_DATASET_EXPORT_LIMIT,
|
|
13
|
+
MAX_DATASET_EXPORT_LIMIT,
|
|
14
|
+
} from './dataset-export.js';
|
|
15
|
+
import { formatPricingStalenessLine } from './pricing-lifecycle.js';
|
|
16
|
+
import type { FleetMode, SmartRouterCommand, SmartRouterRuntime } from './types.js';
|
|
17
|
+
|
|
18
|
+
export function parseHistoryLimit(raw: string | undefined): number {
|
|
19
|
+
if (raw === undefined) {
|
|
20
|
+
return DEFAULT_HISTORY_LIMIT;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parsed = Number.parseInt(raw, 10);
|
|
24
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
25
|
+
throw new Error(`Usage: ${SMART_ROUTER_USAGE}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return Math.min(parsed, MAX_HISTORY_LIMIT);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseExportLimit(tokens: string[]): number {
|
|
32
|
+
let limit = DEFAULT_DATASET_EXPORT_LIMIT;
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
35
|
+
const token = tokens[i];
|
|
36
|
+
if (token === '--limit') {
|
|
37
|
+
const raw = tokens[i + 1];
|
|
38
|
+
if (raw === undefined) {
|
|
39
|
+
throw new Error(`Usage: ${SMART_ROUTER_USAGE}`);
|
|
40
|
+
}
|
|
41
|
+
const parsed = Number.parseInt(raw, 10);
|
|
42
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
43
|
+
throw new Error(`Usage: ${SMART_ROUTER_USAGE}`);
|
|
44
|
+
}
|
|
45
|
+
limit = Math.min(parsed, MAX_DATASET_EXPORT_LIMIT);
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (token?.startsWith('--limit=')) {
|
|
51
|
+
const raw = token.slice('--limit='.length);
|
|
52
|
+
const parsed = Number.parseInt(raw, 10);
|
|
53
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
54
|
+
throw new Error(`Usage: ${SMART_ROUTER_USAGE}`);
|
|
55
|
+
}
|
|
56
|
+
limit = Math.min(parsed, MAX_DATASET_EXPORT_LIMIT);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error(`Usage: ${SMART_ROUTER_USAGE}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return limit;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseSmartRouterArgs(args: string): SmartRouterCommand {
|
|
67
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
68
|
+
if (tokens.length === 0 || tokens[0] === 'status') {
|
|
69
|
+
return { command: 'status' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (tokens[0] === 'history') {
|
|
73
|
+
return { command: 'history', limit: parseHistoryLimit(tokens[1]) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (tokens[0] === 'mode' && (tokens[1] === 'scoped' || tokens[1] === 'all')) {
|
|
77
|
+
return { command: 'mode', mode: tokens[1] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (tokens[0] === 'pricing' && tokens[1] === 'refresh') {
|
|
81
|
+
return { command: 'pricing', subcommand: 'refresh' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (tokens[0] === 'export' && tokens[1] === 'dataset') {
|
|
85
|
+
return {
|
|
86
|
+
command: 'export',
|
|
87
|
+
subcommand: 'dataset',
|
|
88
|
+
limit: parseExportLimit(tokens.slice(2)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (tokens[0] === 'export' && tokens[1] === 'telemetry-contrib') {
|
|
93
|
+
const { limit } = parseExportTelemetryContribArgs(tokens.join(' '));
|
|
94
|
+
return {
|
|
95
|
+
command: 'export',
|
|
96
|
+
subcommand: 'telemetry-contrib',
|
|
97
|
+
limit: Math.min(limit, DEFAULT_TELEMETRY_CONTRIB_EXPORT_LIMIT),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (tokens[0] === 'feedback' && (tokens[1] === 'good' || tokens[1] === 'bad')) {
|
|
102
|
+
return { command: 'feedback', rating: tokens[1] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (tokens[0] === 'unpin' && tokens.length === 1) {
|
|
106
|
+
return { command: 'unpin' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error(`Usage: ${SMART_ROUTER_USAGE}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatStatusMessage(
|
|
113
|
+
runtime: SmartRouterRuntime,
|
|
114
|
+
decision: RoutingDecision | undefined,
|
|
115
|
+
): string {
|
|
116
|
+
const lines = [
|
|
117
|
+
`Fleet mode: ${runtime.fleetMode}`,
|
|
118
|
+
`Fleet size: ${runtime.streamDeps.fleet.length}`,
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const fleetMembers = runtime.streamDeps.fleet
|
|
122
|
+
.map((profile) => `${profile.provider}/${profile.id}`)
|
|
123
|
+
.sort();
|
|
124
|
+
if (fleetMembers.length > 0) {
|
|
125
|
+
lines.push('Fleet members:');
|
|
126
|
+
for (const member of fleetMembers) {
|
|
127
|
+
lines.push(` - ${member}`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
lines.push('Fleet members: (none)');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const stalenessLine = formatPricingStalenessLine(runtime.priceCatalog);
|
|
134
|
+
if (stalenessLine) {
|
|
135
|
+
lines.push(`Pricing: ${stalenessLine}`);
|
|
136
|
+
} else if (runtime.priceCatalog) {
|
|
137
|
+
lines.push(`Pricing: fresh (last_updated ${runtime.priceCatalog.last_updated})`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!decision) {
|
|
141
|
+
lines.push('Last routing decision: (none yet)');
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
lines.push(
|
|
146
|
+
`Model: ${decision.selected_model_id}`,
|
|
147
|
+
`Stage: ${decision.stage}`,
|
|
148
|
+
`Reason: ${decision.reason_code}`,
|
|
149
|
+
`Latency: ${decision.routing_latency_ms}ms`,
|
|
150
|
+
);
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function formatHistoryMessage(entries: readonly RoutingTelemetry[]): string {
|
|
155
|
+
if (entries.length === 0) {
|
|
156
|
+
return 'No routing history yet.';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return entries
|
|
160
|
+
.map(
|
|
161
|
+
(entry) =>
|
|
162
|
+
`${entry.timestamp} | ${entry.selected_model_id} | ${entry.stage} | ${entry.turn_type} | ${entry.routing_latency_ms}ms`,
|
|
163
|
+
)
|
|
164
|
+
.join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type { FleetMode };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
formatHistoryMessage,
|
|
5
|
+
formatStatusMessage,
|
|
6
|
+
parseSmartRouterArgs,
|
|
7
|
+
} from './command-formatters.js';
|
|
8
|
+
import { exportDatasetToFile } from './dataset-export.js';
|
|
9
|
+
import { exportTelemetryContrib } from '../../../src/cli/smart-router-cli.js';
|
|
10
|
+
import { bindSharedModelRegistry, rebuildFleet } from './fleet-bootstrap.js';
|
|
11
|
+
import { refreshPricingCatalog } from './pricing-lifecycle.js';
|
|
12
|
+
import { FLEET_MODE_ENTRY_TYPE } from './session-lifecycle.js';
|
|
13
|
+
import type { SmartRouterRuntime } from './types.js';
|
|
14
|
+
|
|
15
|
+
export const SMART_ROUTER_USAGE =
|
|
16
|
+
'/smart-router [status] | history [limit] | mode scoped|all | pricing refresh | export dataset [--limit N] | export telemetry-contrib [--limit N] | feedback good|bad | unpin';
|
|
17
|
+
|
|
18
|
+
type CompletionItem = { value: string; label: string };
|
|
19
|
+
|
|
20
|
+
const TOP_LEVEL: CompletionItem[] = [
|
|
21
|
+
{ value: 'status', label: 'Show last routing decision' },
|
|
22
|
+
{ value: 'history', label: 'Show recent routing history' },
|
|
23
|
+
{ value: 'mode', label: 'Switch fleet mode (scoped or all)' },
|
|
24
|
+
{ value: 'pricing', label: 'Manage pricing catalog' },
|
|
25
|
+
{ value: 'export', label: 'Export opt-in routing dataset' },
|
|
26
|
+
{ value: 'feedback', label: 'Label last routing outcome good or bad' },
|
|
27
|
+
{ value: 'unpin', label: 'Clear current session pin' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const MODE_COMPLETIONS: CompletionItem[] = [
|
|
31
|
+
{ value: 'mode scoped', label: 'Route among scoped models only' },
|
|
32
|
+
{ value: 'mode all', label: 'Route among all authenticated models' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const PRICING_COMPLETIONS: CompletionItem[] = [
|
|
36
|
+
{ value: 'pricing refresh', label: 'Fetch LiteLLM rates and rebuild fleet' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const EXPORT_COMPLETIONS: CompletionItem[] = [
|
|
40
|
+
{ value: 'export dataset', label: 'Export privacy-safe dataset JSONL' },
|
|
41
|
+
{ value: 'export telemetry-contrib', label: 'Export community telemetry JSON' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const FEEDBACK_COMPLETIONS: CompletionItem[] = [
|
|
45
|
+
{ value: 'feedback good', label: 'Mark last routing outcome as good' },
|
|
46
|
+
{ value: 'feedback bad', label: 'Mark last routing outcome as bad' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Full invocations used to keep completions and parseSmartRouterArgs in sync. */
|
|
50
|
+
export const SMART_ROUTER_FULL_INVOCATIONS = [
|
|
51
|
+
'',
|
|
52
|
+
'status',
|
|
53
|
+
'history',
|
|
54
|
+
'history 10',
|
|
55
|
+
'mode scoped',
|
|
56
|
+
'mode all',
|
|
57
|
+
'pricing refresh',
|
|
58
|
+
'export dataset',
|
|
59
|
+
'export dataset --limit 100',
|
|
60
|
+
'export telemetry-contrib',
|
|
61
|
+
'export telemetry-contrib --limit 100',
|
|
62
|
+
'feedback good',
|
|
63
|
+
'feedback bad',
|
|
64
|
+
'unpin',
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
function filterByPrefix(items: CompletionItem[], prefix: string): CompletionItem[] {
|
|
68
|
+
return items.filter((item) => item.value.startsWith(prefix));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getSmartRouterArgumentCompletions(prefix: string): CompletionItem[] | null {
|
|
72
|
+
const trimmed = prefix.trimStart();
|
|
73
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
74
|
+
|
|
75
|
+
if (tokens[0] === 'mode') {
|
|
76
|
+
const subPrefix = tokens.slice(1).join(' ');
|
|
77
|
+
const filtered = filterByPrefix(MODE_COMPLETIONS, `mode${subPrefix ? ` ${subPrefix}` : ''}`);
|
|
78
|
+
return filtered.length > 0 ? filtered : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (tokens[0] === 'pricing') {
|
|
82
|
+
const subPrefix = tokens.slice(1).join(' ');
|
|
83
|
+
const filtered = filterByPrefix(PRICING_COMPLETIONS, `pricing${subPrefix ? ` ${subPrefix}` : ''}`);
|
|
84
|
+
return filtered.length > 0 ? filtered : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (tokens[0] === 'export') {
|
|
88
|
+
const subPrefix = tokens.slice(1).join(' ');
|
|
89
|
+
const filtered = filterByPrefix(EXPORT_COMPLETIONS, `export${subPrefix ? ` ${subPrefix}` : ''}`);
|
|
90
|
+
return filtered.length > 0 ? filtered : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (tokens[0] === 'feedback') {
|
|
94
|
+
const subPrefix = tokens.slice(1).join(' ');
|
|
95
|
+
const filtered = filterByPrefix(FEEDBACK_COMPLETIONS, `feedback${subPrefix ? ` ${subPrefix}` : ''}`);
|
|
96
|
+
return filtered.length > 0 ? filtered : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (tokens[0] === 'history') {
|
|
100
|
+
return [{ value: 'history', label: 'Show recent routing history' }];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const firstToken = tokens[0] ?? '';
|
|
104
|
+
const filtered = filterByPrefix(TOP_LEVEL, firstToken);
|
|
105
|
+
return filtered.length > 0 ? filtered : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function registerSmartRouterCommand(
|
|
109
|
+
pi: ExtensionAPI,
|
|
110
|
+
runtime: SmartRouterRuntime,
|
|
111
|
+
): void {
|
|
112
|
+
pi.registerCommand('smart-router', {
|
|
113
|
+
description:
|
|
114
|
+
'Show routing status/history, switch fleet mode (scoped|all), refresh pricing, or export dataset',
|
|
115
|
+
getArgumentCompletions: getSmartRouterArgumentCompletions,
|
|
116
|
+
handler: async (args, ctx) => {
|
|
117
|
+
try {
|
|
118
|
+
bindSharedModelRegistry(runtime, ctx.modelRegistry);
|
|
119
|
+
const parsed = parseSmartRouterArgs(args);
|
|
120
|
+
|
|
121
|
+
if (parsed.command === 'status') {
|
|
122
|
+
ctx.ui.notify(formatStatusMessage(runtime, runtime.lastDecision), 'info');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parsed.command === 'history') {
|
|
127
|
+
const rows = await runtime.store.listTelemetry({ limit: parsed.limit });
|
|
128
|
+
ctx.ui.notify(formatHistoryMessage(rows), 'info');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (parsed.command === 'pricing') {
|
|
133
|
+
const { modelCount, lastUpdated } = await refreshPricingCatalog(runtime);
|
|
134
|
+
await rebuildFleet(runtime, pi, ctx.cwd);
|
|
135
|
+
ctx.ui.notify(
|
|
136
|
+
`Pricing refreshed: ${modelCount} models loaded (last_updated: ${lastUpdated}). Fleet rebuilt (${runtime.streamDeps.fleet.length} models).`,
|
|
137
|
+
'info',
|
|
138
|
+
);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (parsed.command === 'export' && parsed.subcommand === 'dataset') {
|
|
143
|
+
const result = await exportDatasetToFile(runtime.store, ctx.cwd, parsed.limit);
|
|
144
|
+
if (!result) {
|
|
145
|
+
ctx.ui.notify('No routing dataset records to export.', 'info');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
ctx.ui.notify(
|
|
149
|
+
`Exported ${result.recordCount} dataset record(s) to ${result.path}`,
|
|
150
|
+
'info',
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (parsed.command === 'export' && parsed.subcommand === 'telemetry-contrib') {
|
|
156
|
+
const result = await exportTelemetryContrib({
|
|
157
|
+
store: runtime.store,
|
|
158
|
+
cwd: ctx.cwd,
|
|
159
|
+
limit: parsed.limit,
|
|
160
|
+
});
|
|
161
|
+
if (!result.path) {
|
|
162
|
+
ctx.ui.notify(
|
|
163
|
+
result.recordCount === 0
|
|
164
|
+
? 'No telemetry-contrib records to export (opt in with SMART_ROUTER_DATASET=1).'
|
|
165
|
+
: 'No telemetry-contrib records written.',
|
|
166
|
+
'info',
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
ctx.ui.notify(
|
|
171
|
+
`Exported ${result.recordCount} telemetry-contrib record(s) to ${result.path}`,
|
|
172
|
+
'info',
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (parsed.command === 'feedback') {
|
|
178
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
179
|
+
const snapshot = runtime.sessionRouting.get(sessionId);
|
|
180
|
+
if (!snapshot) {
|
|
181
|
+
ctx.ui.notify('No recent auto-routed request to label.', 'info');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const record = runtime.outcomeRecorder?.recordFeedback(
|
|
186
|
+
snapshot,
|
|
187
|
+
sessionId,
|
|
188
|
+
parsed.rating,
|
|
189
|
+
);
|
|
190
|
+
if (!record) {
|
|
191
|
+
ctx.ui.notify('Outcome labels require SMART_ROUTER_DATASET=1.', 'info');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
ctx.ui.notify(
|
|
196
|
+
`Recorded ${parsed.rating} feedback for request ${snapshot.lastRequestId}.`,
|
|
197
|
+
'info',
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (parsed.command === 'unpin') {
|
|
203
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
204
|
+
const sessionPinner = runtime.streamDeps.sessionPinner;
|
|
205
|
+
if (!sessionPinner) {
|
|
206
|
+
ctx.ui.notify('Session pinner unavailable.', 'error');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const pin = sessionPinner.getPin(sessionId);
|
|
211
|
+
if (!pin) {
|
|
212
|
+
ctx.ui.notify('No session pin to clear.', 'info');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
sessionPinner.breakPin(sessionId);
|
|
217
|
+
ctx.ui.notify(
|
|
218
|
+
`Cleared session pin (was ${pin.pinned_model_id}). Next request will run full routing.`,
|
|
219
|
+
'info',
|
|
220
|
+
);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (parsed.mode === runtime.fleetMode) {
|
|
225
|
+
ctx.ui.notify(`Fleet mode already set to ${parsed.mode}`, 'info');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
runtime.fleetMode = parsed.mode;
|
|
230
|
+
await rebuildFleet(runtime, pi, ctx.cwd);
|
|
231
|
+
pi.appendEntry(FLEET_MODE_ENTRY_TYPE, { mode: parsed.mode });
|
|
232
|
+
ctx.ui.notify(
|
|
233
|
+
`Fleet mode set to ${parsed.mode} (${runtime.streamDeps.fleet.length} models)`,
|
|
234
|
+
'info',
|
|
235
|
+
);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
ctx.ui.notify(message, 'error');
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
attachOutcomeLabelsToExport,
|
|
8
|
+
indexOutcomesByRequestId,
|
|
9
|
+
} from '../../../src/domain/routing/p-success-classifier.js';
|
|
10
|
+
import { DATASET_MAX_ENTRIES } from '../../../src/infrastructure/telemetry/dataset-limits.js';
|
|
11
|
+
import {
|
|
12
|
+
DatasetRecorder,
|
|
13
|
+
DATASET_ENABLED_NOTIFY_MESSAGE,
|
|
14
|
+
} from '../../../src/infrastructure/telemetry/dataset-recorder.js';
|
|
15
|
+
import { OutcomeRecorder } from '../../../src/infrastructure/telemetry/outcome-recorder.js';
|
|
16
|
+
import type { RoutingDatasetRecord, RoutingOutcomeRecord } from '../../../src/domain/types/index.js';
|
|
17
|
+
import type { StorePort } from '../../../src/domain/types/store-port.js';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_DATASET_EXPORT_DIR = '.pi-smart-router/exports';
|
|
20
|
+
const DEFAULT_DATASET_EXPORT_LIMIT = DATASET_MAX_ENTRIES;
|
|
21
|
+
const MAX_DATASET_EXPORT_LIMIT = DATASET_MAX_ENTRIES;
|
|
22
|
+
|
|
23
|
+
export const DATASET_EXPORT_FORBIDDEN_KEYS = [
|
|
24
|
+
'session_id',
|
|
25
|
+
'prompt_text',
|
|
26
|
+
'messages',
|
|
27
|
+
'prompt',
|
|
28
|
+
'pepper',
|
|
29
|
+
'install_pepper',
|
|
30
|
+
'dataset_pepper',
|
|
31
|
+
'dataset_key',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export function createExtensionDatasetRecorder(
|
|
35
|
+
store: StorePort,
|
|
36
|
+
cwdOrNotify?: string | ((message: string) => void),
|
|
37
|
+
notifyEnabled?: (message: string) => void,
|
|
38
|
+
): DatasetRecorder {
|
|
39
|
+
let cwd = process.cwd();
|
|
40
|
+
let notify: ((message: string) => void) | undefined;
|
|
41
|
+
|
|
42
|
+
if (typeof cwdOrNotify === 'string') {
|
|
43
|
+
cwd = cwdOrNotify;
|
|
44
|
+
notify = notifyEnabled;
|
|
45
|
+
} else if (typeof cwdOrNotify === 'function') {
|
|
46
|
+
notify = cwdOrNotify;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return new DatasetRecorder({
|
|
50
|
+
cwd,
|
|
51
|
+
onRecord: (record) => {
|
|
52
|
+
store.appendDatasetRecord(record);
|
|
53
|
+
},
|
|
54
|
+
onFirstEnable: () => {
|
|
55
|
+
notify?.(DATASET_ENABLED_NOTIFY_MESSAGE);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createExtensionOutcomeRecorder(store: StorePort): OutcomeRecorder {
|
|
61
|
+
return new OutcomeRecorder({
|
|
62
|
+
onRecord: (record) => {
|
|
63
|
+
store.appendOutcomeRecord(record);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function hashSessionIdForExport(sessionId: string): string {
|
|
69
|
+
return createHash('sha256').update(sessionId).digest('hex');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Map a stored dataset row to a privacy-safe JSON export object (Tier 1 only). */
|
|
73
|
+
export function toDatasetExportRecord(
|
|
74
|
+
record: RoutingDatasetRecord,
|
|
75
|
+
): Record<string, unknown> {
|
|
76
|
+
const raw = record as RoutingDatasetRecord & Record<string, unknown>;
|
|
77
|
+
const exportable: Record<string, unknown> = { ...record };
|
|
78
|
+
|
|
79
|
+
for (const key of DATASET_EXPORT_FORBIDDEN_KEYS) {
|
|
80
|
+
delete exportable[key];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sessionId = raw.session_id;
|
|
84
|
+
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
|
85
|
+
delete exportable.session_id;
|
|
86
|
+
exportable.session_id_hash = hashSessionIdForExport(sessionId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return exportable;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatDatasetExportJsonl(
|
|
93
|
+
records: readonly RoutingDatasetRecord[],
|
|
94
|
+
outcomeRecords: readonly RoutingOutcomeRecord[] = [],
|
|
95
|
+
): string {
|
|
96
|
+
const outcomesByRequest = indexOutcomesByRequestId(outcomeRecords);
|
|
97
|
+
|
|
98
|
+
return records
|
|
99
|
+
.map((record) => {
|
|
100
|
+
const exportable = toDatasetExportRecord(record);
|
|
101
|
+
const linkedOutcomes = outcomesByRequest.get(record.request_id) ?? [];
|
|
102
|
+
return JSON.stringify(attachOutcomeLabelsToExport(exportable, linkedOutcomes));
|
|
103
|
+
})
|
|
104
|
+
.join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatDatasetExportTimestamp(date: Date = new Date()): string {
|
|
108
|
+
return date.toISOString().replace(/[:.]/g, '-');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getDatasetExportPath(cwd: string, timestamp: string): string {
|
|
112
|
+
return join(cwd, DEFAULT_DATASET_EXPORT_DIR, `dataset-${timestamp}.jsonl`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface DatasetExportResult {
|
|
116
|
+
readonly path: string;
|
|
117
|
+
readonly recordCount: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function exportDatasetToFile(
|
|
121
|
+
store: StorePort,
|
|
122
|
+
cwd: string,
|
|
123
|
+
limit: number,
|
|
124
|
+
): Promise<DatasetExportResult | null> {
|
|
125
|
+
const records = await store.listDatasetRecords({ limit });
|
|
126
|
+
if (records.length === 0) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const outcomeRecords = await store.listOutcomeRecords({ limit });
|
|
131
|
+
|
|
132
|
+
const timestamp = formatDatasetExportTimestamp();
|
|
133
|
+
const exportDir = join(cwd, DEFAULT_DATASET_EXPORT_DIR);
|
|
134
|
+
mkdirSync(exportDir, { recursive: true });
|
|
135
|
+
const exportPath = getDatasetExportPath(cwd, timestamp);
|
|
136
|
+
const jsonl = formatDatasetExportJsonl(records, outcomeRecords);
|
|
137
|
+
writeFileSync(exportPath, jsonl.length > 0 ? `${jsonl}\n` : '', 'utf8');
|
|
138
|
+
|
|
139
|
+
return { path: exportPath, recordCount: records.length };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { DEFAULT_DATASET_EXPORT_LIMIT, MAX_DATASET_EXPORT_LIMIT };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Api,
|
|
3
|
+
type AssistantMessage,
|
|
4
|
+
type AssistantMessageEvent,
|
|
5
|
+
type Context,
|
|
6
|
+
type Model,
|
|
7
|
+
type SimpleStreamOptions,
|
|
8
|
+
streamSimple as defaultDelegateStream,
|
|
9
|
+
} from '@earendil-works/pi-ai/compat';
|
|
10
|
+
|
|
11
|
+
import { parseAssistantMessageError } from '../../../src/infrastructure/delegation/provider-error.js';
|
|
12
|
+
import {
|
|
13
|
+
buildDelegationContext,
|
|
14
|
+
modelToExecutionModel,
|
|
15
|
+
resolveDelegationOptions,
|
|
16
|
+
type DelegatedStreamResult,
|
|
17
|
+
type DelegationHeadroomContext,
|
|
18
|
+
} from './delegation-runtime.js';
|
|
19
|
+
import type { StreamDelegationDeps } from './types.js';
|
|
20
|
+
|
|
21
|
+
export async function collectDelegatedStream(
|
|
22
|
+
targetModel: Model<Api>,
|
|
23
|
+
context: Context,
|
|
24
|
+
deps: StreamDelegationDeps,
|
|
25
|
+
options: SimpleStreamOptions | undefined,
|
|
26
|
+
headroomContext?: DelegationHeadroomContext,
|
|
27
|
+
): Promise<DelegatedStreamResult> {
|
|
28
|
+
if (options?.signal?.aborted) {
|
|
29
|
+
throw new Error('Request was aborted');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const delegationOptions = await resolveDelegationOptions(
|
|
33
|
+
deps.modelRegistry,
|
|
34
|
+
targetModel,
|
|
35
|
+
options,
|
|
36
|
+
headroomContext,
|
|
37
|
+
);
|
|
38
|
+
const delegateStream = deps.delegateStream ?? defaultDelegateStream;
|
|
39
|
+
const inner = delegateStream(targetModel, context, delegationOptions);
|
|
40
|
+
const events: AssistantMessageEvent[] = [];
|
|
41
|
+
let finalMessage: AssistantMessage | undefined;
|
|
42
|
+
|
|
43
|
+
for await (const event of inner) {
|
|
44
|
+
if (options?.signal?.aborted) {
|
|
45
|
+
throw new Error('Request was aborted');
|
|
46
|
+
}
|
|
47
|
+
events.push(event);
|
|
48
|
+
|
|
49
|
+
if (event.type === 'done') {
|
|
50
|
+
finalMessage = event.message;
|
|
51
|
+
} else if (event.type === 'error') {
|
|
52
|
+
finalMessage = event.error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const failed =
|
|
57
|
+
finalMessage !== undefined &&
|
|
58
|
+
(finalMessage.stopReason === 'error' || finalMessage.stopReason === 'aborted');
|
|
59
|
+
|
|
60
|
+
return { finalMessage, failed, events };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function delegateWithOutcome(
|
|
64
|
+
targetModel: Model<Api>,
|
|
65
|
+
context: Context,
|
|
66
|
+
deps: StreamDelegationDeps,
|
|
67
|
+
options: SimpleStreamOptions | undefined,
|
|
68
|
+
sessionId: string | undefined,
|
|
69
|
+
headroomContext?: DelegationHeadroomContext,
|
|
70
|
+
): Promise<DelegatedStreamResult> {
|
|
71
|
+
const delegationContext = buildDelegationContext(
|
|
72
|
+
context,
|
|
73
|
+
targetModel,
|
|
74
|
+
deps,
|
|
75
|
+
sessionId,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const result = await collectDelegatedStream(
|
|
79
|
+
targetModel,
|
|
80
|
+
delegationContext,
|
|
81
|
+
deps,
|
|
82
|
+
options,
|
|
83
|
+
headroomContext,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!result.finalMessage) {
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (result.failed) {
|
|
91
|
+
const parsed = parseAssistantMessageError(result.finalMessage);
|
|
92
|
+
deps.router.dispatch.recordOutcome(targetModel.id, parsed);
|
|
93
|
+
} else {
|
|
94
|
+
deps.router.dispatch.recordOutcome(targetModel.id);
|
|
95
|
+
if (sessionId) {
|
|
96
|
+
deps.executionLedger.recordSuccess(sessionId, modelToExecutionModel(targetModel));
|
|
97
|
+
}
|
|
98
|
+
deps.onDelegatedModel?.({
|
|
99
|
+
provider: targetModel.provider,
|
|
100
|
+
id: targetModel.id,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|