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,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic cluster matcher — SP-101, GitHub #56.
|
|
3
|
+
*
|
|
4
|
+
* Embeds request prompt text via shared TextEmbedder, scores cosine similarity
|
|
5
|
+
* against precomputed cluster centroids, and returns best match with confidence
|
|
6
|
+
* from per-cluster min_similarity and min_margin thresholds.
|
|
7
|
+
*
|
|
8
|
+
* Centroids load from `config/routing-centroids.json` at startup when present;
|
|
9
|
+
* otherwise they are computed inline from reference prompts (SP-114).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
loadRoutingClusters,
|
|
17
|
+
parseRoutingClustersYaml,
|
|
18
|
+
} from '../../config/routing-clusters-loader.js';
|
|
19
|
+
import { EMBEDDING_DIM, type TextEmbedder } from './embedding-provider.js';
|
|
20
|
+
import type {
|
|
21
|
+
ClusterMatchTableEntry,
|
|
22
|
+
LoadedRoutingCluster,
|
|
23
|
+
RoutingCluster,
|
|
24
|
+
RoutingClusterCatalog,
|
|
25
|
+
RoutingRequest,
|
|
26
|
+
Tier,
|
|
27
|
+
} from '../types/index.js';
|
|
28
|
+
|
|
29
|
+
// ─── Precomputed centroid artifact (SP-114) ──────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_ROUTING_CENTROIDS_PATH = 'config/routing-centroids.json';
|
|
32
|
+
|
|
33
|
+
export interface RoutingCentroidRecord {
|
|
34
|
+
readonly cluster_id: string;
|
|
35
|
+
readonly tier_bias: Tier;
|
|
36
|
+
readonly centroid: readonly number[];
|
|
37
|
+
readonly reference_count: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RoutingCentroidsArtifact {
|
|
41
|
+
readonly version: number;
|
|
42
|
+
readonly embedding_dim: number;
|
|
43
|
+
readonly clusters: readonly RoutingCentroidRecord[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class RoutingCentroidsError extends Error {
|
|
47
|
+
override readonly name = 'RoutingCentroidsError';
|
|
48
|
+
|
|
49
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
50
|
+
super(message, options);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function computeCentroid(embeddings: readonly Float32Array[]): Float32Array {
|
|
55
|
+
if (embeddings.length === 0) {
|
|
56
|
+
throw new RoutingCentroidsError('Cannot compute centroid from zero embeddings');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dim = embeddings[0]!.length;
|
|
60
|
+
const centroid = new Float32Array(dim);
|
|
61
|
+
|
|
62
|
+
for (const embedding of embeddings) {
|
|
63
|
+
if (embedding.length !== dim) {
|
|
64
|
+
throw new RoutingCentroidsError(
|
|
65
|
+
`Embedding shape mismatch: expected ${dim}, got ${embedding.length}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < dim; i++) {
|
|
69
|
+
centroid[i] = (centroid[i] ?? 0) + (embedding[i] ?? 0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < dim; i++) {
|
|
74
|
+
centroid[i] = (centroid[i] ?? 0) / embeddings.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return centroid;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseRoutingCentroidsArtifact(parsed: unknown): RoutingCentroidsArtifact {
|
|
81
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
82
|
+
throw new RoutingCentroidsError('Invalid routing centroids artifact: expected object');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const record = parsed as Record<string, unknown>;
|
|
86
|
+
const version = record.version;
|
|
87
|
+
const embeddingDim = record.embedding_dim;
|
|
88
|
+
const clusters = record.clusters;
|
|
89
|
+
|
|
90
|
+
if (version !== 1) {
|
|
91
|
+
throw new RoutingCentroidsError(
|
|
92
|
+
`Unsupported routing centroids version: ${String(version)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (embeddingDim !== EMBEDDING_DIM) {
|
|
96
|
+
throw new RoutingCentroidsError(
|
|
97
|
+
`Invalid embedding_dim: expected ${EMBEDDING_DIM}, got ${String(embeddingDim)}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(clusters) || clusters.length === 0) {
|
|
101
|
+
throw new RoutingCentroidsError('Invalid routing centroids artifact: clusters required');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parsedClusters: RoutingCentroidRecord[] = [];
|
|
105
|
+
for (const entry of clusters) {
|
|
106
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
107
|
+
throw new RoutingCentroidsError('Invalid cluster entry in routing centroids artifact');
|
|
108
|
+
}
|
|
109
|
+
const cluster = entry as Record<string, unknown>;
|
|
110
|
+
const clusterId = cluster.cluster_id;
|
|
111
|
+
const tierBias = cluster.tier_bias;
|
|
112
|
+
const centroid = cluster.centroid;
|
|
113
|
+
const referenceCount = cluster.reference_count;
|
|
114
|
+
|
|
115
|
+
if (typeof clusterId !== 'string' || clusterId.length === 0) {
|
|
116
|
+
throw new RoutingCentroidsError('Invalid cluster_id in routing centroids artifact');
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
tierBias !== 'zero-tier' &&
|
|
120
|
+
tierBias !== 'economical-cloud' &&
|
|
121
|
+
tierBias !== 'frontier-cloud'
|
|
122
|
+
) {
|
|
123
|
+
throw new RoutingCentroidsError(
|
|
124
|
+
`Invalid tier_bias for cluster '${clusterId}': ${String(tierBias)}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(centroid) || centroid.length !== EMBEDDING_DIM) {
|
|
128
|
+
throw new RoutingCentroidsError(
|
|
129
|
+
`Invalid centroid for cluster '${clusterId}': expected ${EMBEDDING_DIM} dimensions`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (
|
|
133
|
+
!centroid.every((value) => typeof value === 'number' && Number.isFinite(value))
|
|
134
|
+
) {
|
|
135
|
+
throw new RoutingCentroidsError(
|
|
136
|
+
`Invalid centroid values for cluster '${clusterId}': must be finite numbers`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (typeof referenceCount !== 'number' || !Number.isInteger(referenceCount) || referenceCount < 1) {
|
|
140
|
+
throw new RoutingCentroidsError(
|
|
141
|
+
`Invalid reference_count for cluster '${clusterId}'`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
parsedClusters.push({
|
|
146
|
+
cluster_id: clusterId,
|
|
147
|
+
tier_bias: tierBias,
|
|
148
|
+
centroid,
|
|
149
|
+
reference_count: referenceCount,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
version: 1,
|
|
155
|
+
embedding_dim: EMBEDDING_DIM,
|
|
156
|
+
clusters: parsedClusters,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function loadRoutingCentroidsArtifact(
|
|
161
|
+
filePath: string = resolve(DEFAULT_ROUTING_CENTROIDS_PATH),
|
|
162
|
+
): RoutingCentroidsArtifact {
|
|
163
|
+
let raw: string;
|
|
164
|
+
try {
|
|
165
|
+
raw = readFileSync(filePath, 'utf8');
|
|
166
|
+
} catch (err: unknown) {
|
|
167
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
168
|
+
throw new RoutingCentroidsError(
|
|
169
|
+
`Failed to read routing centroids file: ${message}`,
|
|
170
|
+
{ cause: err },
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let parsed: unknown;
|
|
175
|
+
try {
|
|
176
|
+
parsed = JSON.parse(raw) as unknown;
|
|
177
|
+
} catch (err: unknown) {
|
|
178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
179
|
+
throw new RoutingCentroidsError(`Failed to parse routing centroids JSON: ${message}`, {
|
|
180
|
+
cause: err,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return parseRoutingCentroidsArtifact(parsed);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function validateCentroidClusterIds(
|
|
188
|
+
catalogClusters: readonly RoutingCluster[],
|
|
189
|
+
artifact: RoutingCentroidsArtifact,
|
|
190
|
+
): void {
|
|
191
|
+
const catalogIds = new Set(catalogClusters.map((cluster) => cluster.id));
|
|
192
|
+
const artifactIds = new Set(artifact.clusters.map((cluster) => cluster.cluster_id));
|
|
193
|
+
|
|
194
|
+
for (const id of catalogIds) {
|
|
195
|
+
if (!artifactIds.has(id)) {
|
|
196
|
+
throw new RoutingCentroidsError(
|
|
197
|
+
`Routing centroids artifact missing cluster_id '${id}' from catalog`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const id of artifactIds) {
|
|
203
|
+
if (!catalogIds.has(id)) {
|
|
204
|
+
throw new RoutingCentroidsError(
|
|
205
|
+
`Routing centroids artifact has unknown cluster_id '${id}'`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const catalogById = new Map(catalogClusters.map((cluster) => [cluster.id, cluster]));
|
|
211
|
+
for (const record of artifact.clusters) {
|
|
212
|
+
const catalogCluster = catalogById.get(record.cluster_id)!;
|
|
213
|
+
if (catalogCluster.tier_bias !== record.tier_bias) {
|
|
214
|
+
throw new RoutingCentroidsError(
|
|
215
|
+
`tier_bias mismatch for cluster '${record.cluster_id}': catalog=${catalogCluster.tier_bias}, artifact=${record.tier_bias}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (catalogCluster.reference_prompts.length !== record.reference_count) {
|
|
219
|
+
throw new RoutingCentroidsError(
|
|
220
|
+
`reference_count mismatch for cluster '${record.cluster_id}': catalog=${catalogCluster.reference_prompts.length}, artifact=${record.reference_count}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function applyPrecomputedCentroids(
|
|
227
|
+
catalogClusters: readonly RoutingCluster[],
|
|
228
|
+
artifact: RoutingCentroidsArtifact,
|
|
229
|
+
): readonly LoadedRoutingCluster[] {
|
|
230
|
+
validateCentroidClusterIds(catalogClusters, artifact);
|
|
231
|
+
|
|
232
|
+
const centroidById = new Map(
|
|
233
|
+
artifact.clusters.map((record) => [
|
|
234
|
+
record.cluster_id,
|
|
235
|
+
new Float32Array(record.centroid),
|
|
236
|
+
]),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return catalogClusters.map((cluster) => ({
|
|
240
|
+
...cluster,
|
|
241
|
+
centroid: centroidById.get(cluster.id)!,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function buildRoutingCentroidsArtifact(
|
|
246
|
+
catalogClusters: readonly RoutingCluster[],
|
|
247
|
+
embedder: TextEmbedder,
|
|
248
|
+
): Promise<RoutingCentroidsArtifact> {
|
|
249
|
+
const clusters: RoutingCentroidRecord[] = [];
|
|
250
|
+
|
|
251
|
+
for (const cluster of catalogClusters) {
|
|
252
|
+
const embeddings: Float32Array[] = [];
|
|
253
|
+
for (const prompt of cluster.reference_prompts) {
|
|
254
|
+
embeddings.push(await embedder.embed(prompt));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
clusters.push({
|
|
258
|
+
cluster_id: cluster.id,
|
|
259
|
+
tier_bias: cluster.tier_bias,
|
|
260
|
+
centroid: Array.from(computeCentroid(embeddings)),
|
|
261
|
+
reference_count: cluster.reference_prompts.length,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
version: 1,
|
|
267
|
+
embedding_dim: EMBEDDING_DIM,
|
|
268
|
+
clusters,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function serializeRoutingCentroidsArtifact(
|
|
273
|
+
artifact: RoutingCentroidsArtifact,
|
|
274
|
+
): string {
|
|
275
|
+
return `${JSON.stringify(artifact, null, 2)}\n`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface CreateClusterMatcherOptions {
|
|
279
|
+
readonly clustersFilePath?: string;
|
|
280
|
+
readonly centroidsFilePath?: string;
|
|
281
|
+
readonly embedder: TextEmbedder;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isMissingCentroidsFileError(err: unknown): boolean {
|
|
285
|
+
return (
|
|
286
|
+
err instanceof RoutingCentroidsError &&
|
|
287
|
+
err.cause instanceof Error &&
|
|
288
|
+
'code' in err.cause &&
|
|
289
|
+
err.cause.code === 'ENOENT'
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Load routing cluster catalog with precomputed centroids when the artifact exists.
|
|
295
|
+
* Falls back to inline centroid computation from reference prompts when missing.
|
|
296
|
+
*/
|
|
297
|
+
export async function loadClusterMatcherCatalog(
|
|
298
|
+
options: CreateClusterMatcherOptions,
|
|
299
|
+
): Promise<RoutingClusterCatalog> {
|
|
300
|
+
const clustersFilePath =
|
|
301
|
+
options.clustersFilePath ?? resolve('config', 'routing-clusters.yaml');
|
|
302
|
+
const centroidsFilePath =
|
|
303
|
+
options.centroidsFilePath ?? resolve(DEFAULT_ROUTING_CENTROIDS_PATH);
|
|
304
|
+
|
|
305
|
+
const raw = readFileSync(clustersFilePath, 'utf8');
|
|
306
|
+
const catalogClusters = parseRoutingClustersYaml(raw);
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const artifact = loadRoutingCentroidsArtifact(centroidsFilePath);
|
|
310
|
+
return { clusters: applyPrecomputedCentroids(catalogClusters, artifact) };
|
|
311
|
+
} catch (err: unknown) {
|
|
312
|
+
if (isMissingCentroidsFileError(err)) {
|
|
313
|
+
return loadRoutingClusters({
|
|
314
|
+
filePath: clustersFilePath,
|
|
315
|
+
embedder: options.embedder,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function createClusterMatcher(
|
|
323
|
+
options: CreateClusterMatcherOptions,
|
|
324
|
+
): Promise<ClusterMatcher> {
|
|
325
|
+
const catalog = await loadClusterMatcherCatalog(options);
|
|
326
|
+
return new ClusterMatcher({ catalog, embedder: options.embedder });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── Match result ────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
export type ClusterMatchConfidence = 'high' | 'low' | 'none';
|
|
332
|
+
|
|
333
|
+
export interface ClusterMatchResult {
|
|
334
|
+
readonly clusterId: string;
|
|
335
|
+
readonly tierBias: Tier;
|
|
336
|
+
readonly similarity: number;
|
|
337
|
+
/** sim(best) - sim(second); 0 when only one cluster exists. */
|
|
338
|
+
readonly margin: number;
|
|
339
|
+
readonly confidence: ClusterMatchConfidence;
|
|
340
|
+
readonly elapsedMs: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ─── Scoring helpers ─────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
interface RankedCluster {
|
|
346
|
+
readonly cluster: LoadedRoutingCluster;
|
|
347
|
+
readonly similarity: number;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
|
|
351
|
+
if (a.length !== b.length) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`Embedding shape mismatch: expected ${a.length}, got ${b.length}`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let dot = 0;
|
|
358
|
+
let magA = 0;
|
|
359
|
+
let magB = 0;
|
|
360
|
+
|
|
361
|
+
for (let i = 0; i < a.length; i++) {
|
|
362
|
+
const av = a[i] ?? 0;
|
|
363
|
+
const bv = b[i] ?? 0;
|
|
364
|
+
dot += av * bv;
|
|
365
|
+
magA += av * av;
|
|
366
|
+
magB += bv * bv;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (magA === 0 || magB === 0) {
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function rankClusters(
|
|
377
|
+
embedding: Float32Array,
|
|
378
|
+
clusters: readonly LoadedRoutingCluster[],
|
|
379
|
+
): readonly RankedCluster[] {
|
|
380
|
+
return clusters
|
|
381
|
+
.map((cluster) => ({
|
|
382
|
+
cluster,
|
|
383
|
+
similarity: cosineSimilarity(embedding, cluster.centroid),
|
|
384
|
+
}))
|
|
385
|
+
.sort((a, b) => b.similarity - a.similarity);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function computeConfidence(
|
|
389
|
+
similarity: number,
|
|
390
|
+
margin: number,
|
|
391
|
+
minSimilarity: number,
|
|
392
|
+
minMargin: number,
|
|
393
|
+
): ClusterMatchConfidence {
|
|
394
|
+
if (similarity >= minSimilarity && margin >= minMargin) {
|
|
395
|
+
return 'high';
|
|
396
|
+
}
|
|
397
|
+
// Issue #56: low-confidence matches defer — report none for routing.
|
|
398
|
+
return 'none';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ─── ClusterMatcher ──────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
export interface ClusterMatcherConfig {
|
|
404
|
+
readonly catalog: RoutingClusterCatalog;
|
|
405
|
+
readonly embedder: TextEmbedder;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export class ClusterMatcher {
|
|
409
|
+
private readonly clusters: readonly LoadedRoutingCluster[];
|
|
410
|
+
private readonly embedder: TextEmbedder;
|
|
411
|
+
|
|
412
|
+
constructor(config: ClusterMatcherConfig) {
|
|
413
|
+
if (config.catalog.clusters.length === 0) {
|
|
414
|
+
throw new Error('ClusterMatcher requires at least one loaded cluster');
|
|
415
|
+
}
|
|
416
|
+
this.clusters = config.catalog.clusters;
|
|
417
|
+
this.embedder = config.embedder;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async match(request: RoutingRequest): Promise<ClusterMatchResult> {
|
|
421
|
+
const start = performance.now();
|
|
422
|
+
|
|
423
|
+
const embedding = await this.embedder.embed(request.prompt_text);
|
|
424
|
+
const ranked = rankClusters(embedding, this.clusters);
|
|
425
|
+
const best = ranked[0]!;
|
|
426
|
+
const runnerUp = ranked[1];
|
|
427
|
+
const margin = runnerUp
|
|
428
|
+
? best.similarity - runnerUp.similarity
|
|
429
|
+
: 0;
|
|
430
|
+
|
|
431
|
+
const confidence = computeConfidence(
|
|
432
|
+
best.similarity,
|
|
433
|
+
margin,
|
|
434
|
+
best.cluster.min_similarity,
|
|
435
|
+
best.cluster.min_margin,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
clusterId: best.cluster.id,
|
|
440
|
+
tierBias: best.cluster.tier_bias,
|
|
441
|
+
similarity: best.similarity,
|
|
442
|
+
margin,
|
|
443
|
+
confidence,
|
|
444
|
+
elapsedMs: performance.now() - start,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Score all cluster centroids for explain / telemetry tables (SP-113). */
|
|
449
|
+
async matchTable(request: RoutingRequest): Promise<readonly ClusterMatchTableEntry[]> {
|
|
450
|
+
const embedding = await this.embedder.embed(request.prompt_text);
|
|
451
|
+
const ranked = rankClusters(embedding, this.clusters);
|
|
452
|
+
const best = ranked[0];
|
|
453
|
+
|
|
454
|
+
return ranked.map((entry, index) => {
|
|
455
|
+
const runnerUp = ranked[index + 1];
|
|
456
|
+
const margin = runnerUp ? entry.similarity - runnerUp.similarity : null;
|
|
457
|
+
const confidence = computeConfidence(
|
|
458
|
+
entry.similarity,
|
|
459
|
+
index === 0 ? (margin ?? 0) : 0,
|
|
460
|
+
entry.cluster.min_similarity,
|
|
461
|
+
entry.cluster.min_margin,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
cluster_id: entry.cluster.id,
|
|
466
|
+
tier_bias: entry.cluster.tier_bias,
|
|
467
|
+
similarity: entry.similarity,
|
|
468
|
+
margin: index === 0 ? margin : null,
|
|
469
|
+
confidence: confidence === 'high' ? 'high' : 'none',
|
|
470
|
+
selected: entry === best,
|
|
471
|
+
};
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared MiniLM ONNX text embedder — SP-100.
|
|
3
|
+
*
|
|
4
|
+
* Embeds prompt text via Xenova/all-MiniLM-L6-v2 (384-dim) for HyDRA requirement
|
|
5
|
+
* projection and semantic cluster matching. One ONNX session per instance; share
|
|
6
|
+
* across matchers via a single factory call and coordinated dispose().
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const EMBEDDING_DIM = 384;
|
|
10
|
+
|
|
11
|
+
export interface TextEmbedder {
|
|
12
|
+
embed(text: string): Promise<Float32Array>;
|
|
13
|
+
dispose(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ─── ONNX runtime types ───────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
interface OnnxPipelineOutput {
|
|
19
|
+
readonly data: Float32Array;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type OnnxExtractorFn = (
|
|
23
|
+
text: string,
|
|
24
|
+
options: { readonly pooling: string; readonly normalize: boolean },
|
|
25
|
+
) => Promise<OnnxPipelineOutput>;
|
|
26
|
+
|
|
27
|
+
interface TransformersModule {
|
|
28
|
+
pipeline(
|
|
29
|
+
task: string,
|
|
30
|
+
model: string,
|
|
31
|
+
options: Record<string, unknown>,
|
|
32
|
+
): Promise<OnnxExtractorFn>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a TextEmbedder backed by @huggingface/transformers ONNX runtime.
|
|
37
|
+
* Model: Xenova/all-MiniLM-L6-v2 (384-dim).
|
|
38
|
+
*
|
|
39
|
+
* The package is loaded dynamically — not required at compile time.
|
|
40
|
+
* Install: `npm i @huggingface/transformers`
|
|
41
|
+
*/
|
|
42
|
+
export async function createOnnxTextEmbedder(
|
|
43
|
+
artifactCachePath: string,
|
|
44
|
+
): Promise<TextEmbedder> {
|
|
45
|
+
const moduleName = '@huggingface/transformers';
|
|
46
|
+
let mod: TransformersModule;
|
|
47
|
+
try {
|
|
48
|
+
mod = (await import(moduleName)) as TransformersModule;
|
|
49
|
+
} catch {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`ONNX embedding requires ${moduleName}. Install: npm i ${moduleName}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const extractor: OnnxExtractorFn = await mod.pipeline(
|
|
56
|
+
'feature-extraction',
|
|
57
|
+
'Xenova/all-MiniLM-L6-v2',
|
|
58
|
+
{ cache_dir: artifactCachePath },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
async embed(text: string): Promise<Float32Array> {
|
|
63
|
+
const output = await extractor(text, {
|
|
64
|
+
pooling: 'mean',
|
|
65
|
+
normalize: true,
|
|
66
|
+
});
|
|
67
|
+
if (output.data.length !== EMBEDDING_DIM) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Embedding shape mismatch: expected ${EMBEDDING_DIM}, got ${output.data.length}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return output.data;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async dispose(): Promise<void> {
|
|
76
|
+
/* @huggingface/transformers pipelines have no explicit dispose */
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HyDRA embedding input builder — SP-112, GitHub #60.
|
|
3
|
+
*
|
|
4
|
+
* Prefixes prompt text with session metadata so requirement vectors reflect
|
|
5
|
+
* turn context, not only the latest user string.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { RoutingRequest } from '../types/index.js';
|
|
9
|
+
import type { TriageResult } from '../triage/triage-engine.js';
|
|
10
|
+
|
|
11
|
+
function resolveMessageCount(request: RoutingRequest): number {
|
|
12
|
+
return request.messages?.length ?? 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveHasToolContext(request: RoutingRequest): boolean {
|
|
16
|
+
if (request.turn_type === 'tool_result') {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return request.messages?.some((message) => message.role === 'tool') ?? false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveEstimatedInputTokens(request: RoutingRequest): number {
|
|
24
|
+
return request.estimated_input_tokens ?? 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveTurnType(request: RoutingRequest): string {
|
|
28
|
+
return request.turn_type ?? 'unknown';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build HyDRA encoder input: metadata prefix + prompt text.
|
|
33
|
+
*
|
|
34
|
+
* Format: `[turns:N|tools:0|tokens:N|type:...] {prompt_text}`
|
|
35
|
+
*
|
|
36
|
+
* Metadata affects capability prediction only; tier selection uses the
|
|
37
|
+
* cluster/feature gate (SP-103) separately.
|
|
38
|
+
*/
|
|
39
|
+
export function buildHydraInput(
|
|
40
|
+
request: RoutingRequest,
|
|
41
|
+
triage?: TriageResult,
|
|
42
|
+
): string {
|
|
43
|
+
void triage;
|
|
44
|
+
|
|
45
|
+
const flags = [
|
|
46
|
+
`turns:${resolveMessageCount(request)}`,
|
|
47
|
+
`tools:${resolveHasToolContext(request) ? 1 : 0}`,
|
|
48
|
+
`tokens:${resolveEstimatedInputTokens(request)}`,
|
|
49
|
+
`type:${resolveTurnType(request)}`,
|
|
50
|
+
].join('|');
|
|
51
|
+
|
|
52
|
+
return `[${flags}] ${request.prompt_text}`;
|
|
53
|
+
}
|