pi-smart-router 0.21.0 → 0.22.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.
Files changed (32) hide show
  1. package/.pi/extensions/smart-router/command-formatters.ts +9 -11
  2. package/.pi/extensions/smart-router/commands.ts +5 -2
  3. package/.pi/extensions/smart-router/dataset-export.ts +8 -7
  4. package/.pi/extensions/smart-router/delegate-stream.ts +8 -5
  5. package/.pi/extensions/smart-router/delegation-runtime.ts +8 -11
  6. package/.pi/extensions/smart-router/extension-setup.ts +9 -4
  7. package/.pi/extensions/smart-router/fleet-bootstrap.ts +19 -21
  8. package/.pi/extensions/smart-router/index.ts +5 -4
  9. package/.pi/extensions/smart-router/planning-delegate.ts +9 -8
  10. package/.pi/extensions/smart-router/pricing-lifecycle.ts +9 -4
  11. package/.pi/extensions/smart-router/route-and-delegate.ts +13 -12
  12. package/.pi/extensions/smart-router/routing-context.ts +4 -2
  13. package/.pi/extensions/smart-router/routing-outcomes.ts +6 -2
  14. package/.pi/extensions/smart-router/session-lifecycle.ts +5 -2
  15. package/.pi/extensions/smart-router/stream-delegation.ts +8 -2
  16. package/.pi/extensions/smart-router/types.ts +13 -16
  17. package/.pi/extensions/smart-router/utils.ts +5 -3
  18. package/README.md +28 -2
  19. package/config/benchmark-profiles.json +2 -2
  20. package/config/onnx-artifact-pins.json +18 -0
  21. package/config/operator-config.json.example +1 -0
  22. package/dist/domain/matching/embedding-provider.d.ts +40 -3
  23. package/dist/domain/matching/embedding-provider.d.ts.map +1 -1
  24. package/dist/domain/matching/embedding-provider.js +159 -9
  25. package/dist/domain/matching/embedding-provider.js.map +1 -1
  26. package/dist/index.d.ts +35 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +38 -0
  29. package/dist/index.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/domain/matching/embedding-provider.ts +241 -7
  32. package/src/index.ts +151 -0
@@ -1,23 +1,21 @@
1
1
  import {
2
2
  DEFAULT_HISTORY_LIMIT,
3
3
  MAX_HISTORY_LIMIT,
4
- } from '../../../src/infrastructure/telemetry/telemetry-limits.js';
4
+ aggregateSessionStatsFromFleet,
5
+ DEFAULT_TELEMETRY_CONTRIB_EXPORT_LIMIT,
6
+ parseExportTelemetryContribArgs,
7
+ } from '../../../src/index.js';
5
8
  import type {
6
9
  ModelProfile,
7
10
  PriceCatalog,
8
11
  RoutingDecision,
9
12
  RoutingTelemetry,
10
- } from '../../../src/domain/types/index.js';
11
- import {
12
- aggregateSessionStatsFromFleet,
13
- type SessionStatsSnapshot,
14
- } from '../../../src/infrastructure/telemetry/session-stats.js';
13
+ SessionStatsSnapshot,
14
+ PlacementPlanReport,
15
+ } from '../../../src/index.js';
16
+
15
17
  import { SMART_ROUTER_USAGE } from './commands.js';
16
- import {
17
- DEFAULT_TELEMETRY_CONTRIB_EXPORT_LIMIT,
18
- parseExportTelemetryContribArgs,
19
- } from '../../../src/cli/smart-router-cli.js';
20
- import type { PlacementPlanReport } from '../../../src/infrastructure/hardware/placement-plan.js';
18
+
21
19
  import {
22
20
  DEFAULT_DATASET_EXPORT_LIMIT,
23
21
  MAX_DATASET_EXPORT_LIMIT,
@@ -10,8 +10,11 @@ import {
10
10
  parseSmartRouterArgs,
11
11
  } from './command-formatters.js';
12
12
  import { exportDatasetToFile } from './dataset-export.js';
13
- import { exportTelemetryContrib } from '../../../src/cli/smart-router-cli.js';
14
- import { collectPlacementPlan } from '../../../src/infrastructure/hardware/placement-plan.js';
13
+ import {
14
+ exportTelemetryContrib,
15
+ collectPlacementPlan,
16
+ } from '../../../src/index.js';
17
+
15
18
  import { bindSharedModelRegistry, rebuildFleet } from './fleet-bootstrap.js';
16
19
  import { refreshPricingCatalog } from './pricing-lifecycle.js';
17
20
  import { FLEET_MODE_ENTRY_TYPE } from './session-lifecycle.js';
@@ -6,15 +6,16 @@ import { createHash } from 'node:crypto';
6
6
  import {
7
7
  attachOutcomeLabelsToExport,
8
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 {
9
+ DATASET_MAX_ENTRIES,
12
10
  DatasetRecorder,
13
11
  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';
12
+ OutcomeRecorder,
13
+ } from '../../../src/index.js';
14
+ import type {
15
+ RoutingDatasetRecord,
16
+ RoutingOutcomeRecord,
17
+ StorePort,
18
+ } from '../../../src/index.js';
18
19
 
19
20
  const DEFAULT_DATASET_EXPORT_DIR = '.pi-smart-router/exports';
20
21
  const DEFAULT_DATASET_EXPORT_LIMIT = DATASET_MAX_ENTRIES;
@@ -9,13 +9,16 @@ import {
9
9
  streamSimple as compatDelegateStream,
10
10
  } from '@earendil-works/pi-ai/compat';
11
11
 
12
- import { parseAssistantMessageError } from '../../../src/infrastructure/delegation/provider-error.js';
13
- import { extractUsageActuals } from '../../../src/infrastructure/telemetry/routing-telemetry.js';
14
12
  import {
13
+ parseAssistantMessageError,
14
+ extractUsageActuals,
15
15
  resolveAdaptiveReasoning,
16
- type AdaptiveReasoningResult,
17
- type AdaptiveReasoningSignal,
18
- } from '../../../src/domain/delegation/adaptive-reasoning.js';
16
+ } from '../../../src/index.js';
17
+ import type {
18
+ AdaptiveReasoningResult,
19
+ AdaptiveReasoningSignal,
20
+ } from '../../../src/index.js';
21
+
19
22
  import {
20
23
  buildDelegationContext,
21
24
  forwardDelegatedEvent,
@@ -15,26 +15,23 @@ import type { ModelRegistry } from '@earendil-works/pi-coding-agent';
15
15
 
16
16
  import {
17
17
  applyConcisenessHint,
18
- type AdaptiveReasoningResult,
19
- } from '../../../src/domain/delegation/adaptive-reasoning.js';
20
- import {
21
18
  isGoogleDelegationTarget,
22
19
  normalizeDelegationContext,
23
20
  repairGeminiReplayContext,
24
- } from '../../../src/domain/delegation/delegation-context.js';
25
- import {
26
21
  computeOutputHeadroom,
27
- type OutputHeadroomConfig,
28
- } from '../../../src/domain/delegation/output-headroom.js';
29
- import type { ModelProfile } from '../../../src/domain/types/index.js';
30
- import {
31
22
  formatGeminiThoughtSignatureErrorMessage,
32
23
  formatProviderErrorMessage,
33
24
  isGeminiThoughtSignatureAssistantError,
34
25
  parseAssistantMessageError,
35
26
  sanitizeLengthStopMessage,
36
- type LengthStopHints,
37
- } from '../../../src/infrastructure/delegation/provider-error.js';
27
+ } from '../../../src/index.js';
28
+ import type {
29
+ AdaptiveReasoningResult,
30
+ OutputHeadroomConfig,
31
+ ModelProfile,
32
+ LengthStopHints,
33
+ } from '../../../src/index.js';
34
+
38
35
  import type { StreamDelegationDeps } from './types.js';
39
36
 
40
37
  /** Stream options safe to forward to a delegated provider call. */
@@ -4,10 +4,15 @@ import {
4
4
  type ExtensionAPI,
5
5
  } from '@earendil-works/pi-coding-agent';
6
6
 
7
- import { resolveOperatorConfigFromEnv } from '../../../src/config/defaults.js';
8
- import { ExecutionLedger } from '../../../src/domain/delegation/execution-ledger.js';
9
- import type { SessionRoutingSnapshot } from '../../../src/infrastructure/telemetry/outcome-recorder.js';
10
- import { createRouterFromFleet, LifecycleHookState } from '../../../src/index.js';
7
+ import {
8
+ resolveOperatorConfigFromEnv,
9
+ ExecutionLedger,
10
+ createRouterFromFleet,
11
+ LifecycleHookState,
12
+ } from '../../../src/index.js';
13
+ import type {
14
+ SessionRoutingSnapshot,
15
+ } from '../../../src/index.js';
11
16
 
12
17
  import { registerSmartRouterCommand } from './commands.js';
13
18
  import {
@@ -2,39 +2,37 @@ import type { Api, Model } from '@earendil-works/pi-ai/compat';
2
2
  import {
3
3
  ModelRegistry,
4
4
  SettingsManager,
5
- type ExtensionAPI,
5
+ ExtensionAPI,
6
6
  } from '@earendil-works/pi-coding-agent';
7
7
 
8
- import { mapFleetFromRegistry } from '../../../src/config/pi-model-mapper.js';
9
8
  import {
9
+ mapFleetFromRegistry,
10
10
  DEFAULT_OPERATOR_CONFIG,
11
11
  resolveOperatorConfigFromEnv,
12
- } from '../../../src/config/defaults.js';
13
- import {
14
12
  HydraMatcher,
15
13
  createOnnxEmbeddingProvider,
16
- } from '../../../src/domain/matching/hydra-matcher.js';
17
- import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
18
- import {
14
+ SessionPinner,
19
15
  collectPoolModelIds,
20
16
  resolveQuotaWindowEstimateConfigFromEnv,
21
17
  resolveQuotaWindowPosition,
22
- type QuotaWindowAdapter,
23
- type QuotaWindowEstimateConfig,
24
- } from '../../../src/domain/pricing/quota-window-feed.js';
25
- import type { ModelProfile, PriceCatalog } from '../../../src/domain/types/index.js';
26
- import type { QuotaWindowPosition } from '../../../src/domain/types/entities.js';
27
- import type { OperatorConfig } from '../../../src/domain/types/schemas.js';
28
- import type { StorePort } from '../../../src/domain/types/store-port.js';
29
- import { getDefaultSystemInfo } from '../../../src/infrastructure/hardware/hardware-probe.js';
30
- import { DEFAULT_LOCAL_CONFIG } from '../../../src/infrastructure/local/local-zero-tier.js';
31
- import { RoutingTelemetryEmitter } from '../../../src/infrastructure/telemetry/routing-telemetry.js';
32
- import { applyCatalogPricesToFleet } from '../../../src/infrastructure/pricing/price-broker.js';
33
- import {
18
+ getDefaultSystemInfo,
19
+ DEFAULT_LOCAL_CONFIG,
20
+ RoutingTelemetryEmitter,
21
+ applyCatalogPricesToFleet,
34
22
  createRouterFromFleet,
35
- type GatewayDispatchOptions,
36
- type PiExtensionHooks,
37
23
  } from '../../../src/index.js';
24
+ import type {
25
+ QuotaWindowAdapter,
26
+ QuotaWindowEstimateConfig,
27
+ ModelProfile,
28
+ PriceCatalog,
29
+ QuotaWindowPosition,
30
+ OperatorConfig,
31
+ StorePort,
32
+ GatewayDispatchOptions,
33
+ PiExtensionHooks,
34
+ } from '../../../src/index.js';
35
+
38
36
  import { resolveModelScope } from './pi-model-scope.js';
39
37
  import type { FleetMode, SmartRouterRuntime } from './types.js';
40
38
  import { resolveRateLimiter } from './utils.js';
@@ -6,8 +6,9 @@
6
6
  * hooks for routing state. Stream delegation routes each request through the
7
7
  * pipeline and forwards to the selected provider's built-in streaming API.
8
8
  *
9
- * Imports from ../../../src/** (not dist/) because the extension is loaded by pi
10
- * from source at dev time and is excluded from the npm dist artifact.
9
+ * Imports the package public facade (../../../src/index.js, not dist/) because
10
+ * the extension is loaded by pi from source at dev time and is excluded from
11
+ * the npm dist artifact. Deep src/** subpath imports are forbidden (SP-256/257).
11
12
  */
12
13
 
13
14
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
@@ -116,14 +117,14 @@ export { routeAndDelegate } from './route-and-delegate.js';
116
117
  export {
117
118
  formatGeminiThoughtSignatureErrorMessage,
118
119
  isGeminiThoughtSignatureAssistantError,
119
- } from '../../../src/infrastructure/delegation/provider-error.js';
120
+ } from '../../../src/index.js';
120
121
  export {
121
122
  GEMINI_TOOL_HISTORY_EXCLUDED,
122
123
  hasToolCallHistory,
123
124
  hasToolCallHistoryFromContext,
124
125
  isGoogleGeminiProfile,
125
126
  resolveEffectiveFleet,
126
- } from '../../../src/domain/routing/tool-history-guard.js';
127
+ } from '../../../src/index.js';
127
128
 
128
129
  export default async function smartRouterExtension(pi: ExtensionAPI): Promise<void> {
129
130
  const cwd = process.cwd();
@@ -22,21 +22,22 @@ import {
22
22
  type TextContent,
23
23
  } from '@earendil-works/pi-ai/compat';
24
24
 
25
- import type {
26
- CompressedContextSpec,
27
- PlanningDelegateObservability,
28
- RoutingDecision,
29
- } from '../../../src/domain/types/index.js';
30
- import { resolveAdaptiveReasoning } from '../../../src/domain/delegation/adaptive-reasoning.js';
31
- import { DEFAULT_PLANNING_DELEGATE_CONFIG } from '../../../src/domain/types/schemas.js';
32
25
  import {
26
+ resolveAdaptiveReasoning,
27
+ DEFAULT_PLANNING_DELEGATE_CONFIG,
33
28
  createPlanningDelegateObservability,
34
29
  enrichRoutingDecisionWithPlanningDelegate,
35
30
  PLANNING_DELEGATE,
36
31
  PLANNING_DELEGATE_TIMEOUT,
37
32
  PLANNING_DELEGATE_UNAVAILABLE,
38
33
  PLANNING_DIRECT_FRONTIER,
39
- } from '../../../src/infrastructure/telemetry/routing-telemetry.js';
34
+ } from '../../../src/index.js';
35
+ import type {
36
+ CompressedContextSpec,
37
+ PlanningDelegateObservability,
38
+ RoutingDecision,
39
+ } from '../../../src/index.js';
40
+
40
41
  import { collectDelegatedStream } from './delegate-stream.js';
41
42
  import { findFleetProfile, resolveRegistryModel } from './delegation-runtime.js';
42
43
  import type { StreamDelegationDeps } from './types.js';
@@ -1,7 +1,12 @@
1
- import { DEFAULT_OPERATOR_CONFIG } from '../../../src/config/defaults.js';
2
- import type { PriceCatalog } from '../../../src/domain/types/index.js';
3
- import { fetchLitellmPriceCatalog } from '../../../src/infrastructure/pricing/litellm-fetch.js';
4
- import { checkStaleness } from '../../../src/infrastructure/pricing/pricing-monitor.js';
1
+ import {
2
+ DEFAULT_OPERATOR_CONFIG,
3
+ fetchLitellmPriceCatalog,
4
+ checkStaleness,
5
+ } from '../../../src/index.js';
6
+ import type {
7
+ PriceCatalog,
8
+ } from '../../../src/index.js';
9
+
5
10
  import type { SmartRouterRuntime } from './types.js';
6
11
 
7
12
  export async function refreshPricingCatalog(
@@ -7,27 +7,28 @@ import type {
7
7
  SimpleStreamOptions,
8
8
  } from '@earendil-works/pi-ai/compat';
9
9
 
10
- import { safeCloudDefault } from '../../../src/domain/pipeline/safe-default.js';
11
- import { computeOutputHeadroom } from '../../../src/domain/delegation/output-headroom.js';
12
- import { resolvePeakPricingAdjustment } from '../../../src/domain/pricing/peak-pricing.js';
13
10
  import {
11
+ safeCloudDefault,
12
+ computeOutputHeadroom,
13
+ resolvePeakPricingAdjustment,
14
14
  CONTEXT_OVERFLOW_NO_FIT,
15
15
  resolveContextOverflowFallback,
16
- } from '../../../src/domain/routing/context-fit.js';
17
- import {
18
16
  assertRoutableFleetAfterGeminiToolHistoryGuard,
19
17
  GEMINI_TOOL_HISTORY_EXCLUDED,
20
18
  isGoogleGeminiProfile,
21
19
  resolveEffectiveFleet,
22
- } from '../../../src/domain/routing/tool-history-guard.js';
23
- import type { ModelProfile, RoutingDecision, RoutingRequest } from '../../../src/domain/types/index.js';
24
- import type { GeminiToolHistoryGuardResult } from '../../../src/domain/routing/tool-history-guard.js';
25
- import {
26
20
  isGeminiThoughtSignatureAssistantError,
27
21
  parseAssistantMessageError,
28
- } from '../../../src/infrastructure/delegation/provider-error.js';
29
- import { GEMINI_REPLAY_INCOMPATIBLE } from '../../../src/infra/gemini-provider.js';
30
- import { shouldFailoverOnProviderError } from '../../../src/infrastructure/gateway/gateway-dispatch.js';
22
+ GEMINI_REPLAY_INCOMPATIBLE,
23
+ shouldFailoverOnProviderError,
24
+ } from '../../../src/index.js';
25
+ import type {
26
+ ModelProfile,
27
+ RoutingDecision,
28
+ RoutingRequest,
29
+ GeminiToolHistoryGuardResult,
30
+ } from '../../../src/index.js';
31
+
31
32
  import {
32
33
  commitPipedTerminal,
33
34
  delegateWithOutcome,
@@ -7,12 +7,14 @@ import {
7
7
  type TextContent,
8
8
  } from '@earendil-works/pi-ai/compat';
9
9
 
10
+ import {
11
+ LifecycleHookState,
12
+ } from '../../../src/index.js';
10
13
  import type {
11
14
  Message as RoutingMessage,
12
15
  RoutingRequest,
13
16
  TurnType,
14
- } from '../../../src/domain/types/index.js';
15
- import { LifecycleHookState } from '../../../src/index.js';
17
+ } from '../../../src/index.js';
16
18
 
17
19
  const TOKEN_ESTIMATE_KEYS = [
18
20
  'estimatedInputTokens',
@@ -1,5 +1,9 @@
1
- import type { RoutingDecision, RoutingRequest } from '../../../src/domain/types/index.js';
2
- import type { SessionRoutingSnapshot } from '../../../src/infrastructure/telemetry/outcome-recorder.js';
1
+ import type {
2
+ RoutingDecision,
3
+ RoutingRequest,
4
+ SessionRoutingSnapshot,
5
+ } from '../../../src/index.js';
6
+
3
7
  import type { StreamDelegationDeps } from './types.js';
4
8
 
5
9
  export function capturePreRouteOutcomes(
@@ -1,7 +1,10 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
2
2
 
3
- import { evictInMemorySessionState } from '../../../src/api/session-eviction.js';
4
- import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
3
+ import {
4
+ evictInMemorySessionState,
5
+ SessionPinner,
6
+ } from '../../../src/index.js';
7
+
5
8
  import {
6
9
  bindSharedModelRegistry,
7
10
  ensureFleetFresh,
@@ -7,8 +7,14 @@ import {
7
7
  createAssistantMessageEventStream,
8
8
  } from '@earendil-works/pi-ai/compat';
9
9
 
10
- import type { RoutingDecision, RoutingFeatureSidecar } from '../../../src/domain/types/index.js';
11
- import { resolvePeakPricingAdjustment } from '../../../src/domain/pricing/peak-pricing.js';
10
+ import {
11
+ resolvePeakPricingAdjustment,
12
+ } from '../../../src/index.js';
13
+ import type {
14
+ RoutingDecision,
15
+ RoutingFeatureSidecar,
16
+ } from '../../../src/index.js';
17
+
12
18
  import { createErrorMessage } from './delegation-runtime.js';
13
19
  import { routeAndDelegate } from './route-and-delegate.js';
14
20
  import type { StreamDelegationDeps } from './types.js';
@@ -7,10 +7,15 @@ import type {
7
7
  } from '@earendil-works/pi-ai/compat';
8
8
  import type { ModelRegistry } from '@earendil-works/pi-coding-agent';
9
9
 
10
- import type { HydraMatcher } from '../../../src/domain/matching/hydra-matcher.js';
11
- import { ExecutionLedger } from '../../../src/domain/delegation/execution-ledger.js';
12
- import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
10
+ import {
11
+ ExecutionLedger,
12
+ SessionPinner,
13
+ DatasetRecorder,
14
+ OutcomeRecorder,
15
+ LifecycleHookState,
16
+ } from '../../../src/index.js';
13
17
  import type {
18
+ HydraMatcher,
14
19
  AdaptiveReasoningConfig,
15
20
  ModelProfile,
16
21
  PlanningDelegateConfig,
@@ -18,21 +23,13 @@ import type {
18
23
  RoutingDecision,
19
24
  RoutingReasoningTelemetry,
20
25
  RoutingUsageActuals,
21
- } from '../../../src/domain/types/index.js';
22
- import type { PlanningDelegateSpawnFn } from './planning-delegate.js';
23
- import type { StorePort } from '../../../src/domain/types/store-port.js';
24
- import {
25
- DatasetRecorder,
26
- } from '../../../src/infrastructure/telemetry/dataset-recorder.js';
27
- import {
28
- OutcomeRecorder,
29
- type SessionRoutingSnapshot,
30
- } from '../../../src/infrastructure/telemetry/outcome-recorder.js';
31
- import {
32
- LifecycleHookState,
33
- type RouterHandle,
26
+ StorePort,
27
+ SessionRoutingSnapshot,
28
+ RouterHandle,
34
29
  } from '../../../src/index.js';
35
30
 
31
+ import type { PlanningDelegateSpawnFn } from './planning-delegate.js';
32
+
36
33
  export type FleetMode = 'scoped' | 'all';
37
34
 
38
35
  export type SmartRouterCommand =
@@ -4,9 +4,11 @@ import {
4
4
  createResilientStore,
5
5
  SqliteStore,
6
6
  SqliteStoreError,
7
- } from '../../../src/infrastructure/persistence/sqlite-store.js';
8
- import type { StorePort } from '../../../src/domain/types/store-port.js';
9
- import type { RateLimitPort } from '../../../src/infrastructure/gateway/gateway-dispatch.js';
7
+ } from '../../../src/index.js';
8
+ import type {
9
+ StorePort,
10
+ RateLimitPort,
11
+ } from '../../../src/index.js';
10
12
 
11
13
  export const DEFAULT_ROUTER_STATE_DB_PATH = '.pi-smart-router/state.db';
12
14
  export const DEFAULT_RATE_LIMIT_MAX_TOKENS = 60;
package/README.md CHANGED
@@ -290,7 +290,7 @@ These behaviors run in `.pi/extensions/smart-router/` and have **no equivalent i
290
290
  ### Recommended integration path
291
291
 
292
292
  - **pi users:** install the **extension** (`pi install npm:pi-smart-router`). It is the full product — everything in the table above works out of the box, including failover, delegate spawn, headroom escalation, and quota reaction.
293
- - **npm embedders:** you get the **routing core** (12-stage pipeline, fleet mapping, telemetry, gateway health tracking, failover *selection*). Plan to implement your own stream delegation, failover iteration, headroom checks, and planning-delegate spawn around the decisions the pipeline returns — or track [#149](https://github.com/beettlle/pi-smart-router/issues/149) (**extension public facade**), the migration plan for exposing the extension's stream/delegation surface as supported library API so this gap closes over time. Until #149 lands, the extension modules also import `src/**` internals directly, so deep imports into `src/` are not a stable API.
293
+ - **npm embedders:** you get the **routing core** (12-stage pipeline, fleet mapping, telemetry, gateway health tracking, failover *selection*). Plan to implement your own stream delegation, failover iteration, headroom checks, and planning-delegate spawn around the decisions the pipeline returns — or track [#149](https://github.com/beettlle/pi-smart-router/issues/149) (**extension public facade**), the migration plan for exposing the extension's stream/delegation surface as supported library API so this gap closes over time. Until #149 lands, the extension modules also import `src/**` internals directly, so deep imports into `src/` are not a stable API. See [docs/extension-package-boundary.md](docs/extension-package-boundary.md) for the facade vs internal-API boundary, the deep-import lint guard, and the extension coverage gate.
294
294
 
295
295
  ```text
296
296
  pi extension path (full product) npm library path (routing core)
@@ -972,6 +972,32 @@ npm run benchmark:encoder
972
972
 
973
973
  The script reports p50/p95 latency for each encoder and asserts Granite p50/p95 stay within the 120 ms budget ceiling. Requires `@huggingface/transformers` and a one-time ONNX artifact download.
974
974
 
975
+ #### Supply-chain: artifact pins, offline cache, and audit posture
976
+
977
+ **Digest pinning (SP-259, [#147](https://github.com/beettlle/pi-smart-router/issues/147)).** The embedder verifies cached ONNX artifacts against SHA-256 pins before they are used. Pins live in [`config/onnx-artifact-pins.json`](config/onnx-artifact-pins.json) (`pins[modelId][cacheRelativePath] = sha256`; digests are the HuggingFace LFS oids for the default quantized artifacts). Pin mode is controlled by `SMART_ROUTER_ONNX_PIN_MODE`:
978
+
979
+ | Mode | Behavior |
980
+ |------|----------|
981
+ | `off` (default) | No verification — first run downloads unpinned (local dogfood preserved). |
982
+ | `verify` | Configured pins are verified after load; models without pins still download unpinned. |
983
+ | `enforce` | CI/prod: pins are **required** for the loaded model — missing pin file, missing pins for the model, missing cached artifact, or any digest mismatch all fail closed. |
984
+
985
+ Override the pin file location with `SMART_ROUTER_ONNX_PIN_FILE`. Verification runs **after** the transformers.js pipeline loads but **before** the embedder is returned, so even a first-run anonymous download is checked against configured pins — tampered or unexpected bytes fail closed and the session is never usable. Anonymous fetch does not silently bypass pins when they are configured. When upgrading model revisions, re-fetch digests from the HuggingFace API out-of-band and update the pin file; never pin a digest you have not verified.
986
+
987
+ **Offline / air-gapped cache warm.** The artifact cache (`hydra.artifact_cache_path`, default `.pi-smart-router/models/`) is fully self-contained once warmed:
988
+
989
+ 1. On a networked host, warm the cache with the encoder you deploy — run any routed request, or `npm run benchmark:encoder -- --cache .pi-smart-router/models/`.
990
+ 2. Enable pin mode (`verify` or `enforce`) during the warm so digest mismatches surface on the networked host, before rollout.
991
+ 3. Copy the cache directory (and `config/onnx-artifact-pins.json` when pinning) to the offline host and point `hydra.artifact_cache_path` at the copy.
992
+ 4. Defense in depth: embedders can set `env.allowRemoteModels = false` (from `@huggingface/transformers`) before routing to forbid any network fetch, so no anonymous download path exists in production.
993
+
994
+ **`npm audit` posture for the transformers chain.** The audit baseline includes high-severity advisories in `@huggingface/transformers` → `onnxruntime-node` → `adm-zip` ([GHSA-xcpc-8h2w-3j85](https://github.com/advisories/GHSA-xcpc-8h2w-3j85) — crafted ZIP triggers a 4GB memory allocation) and `sharp` (libvips CVEs), both **with no upstream fix available**. Accepted-risk rationale:
995
+
996
+ - Model bytes in the pinned production path come only from the operator-controlled local cache, verified by SHA-256 — the vulnerable archive-decompression path is not exposed to untrusted input there.
997
+ - Inference runs locally with no gradients and no untrusted deserialization; untrusted bytes enter only on first cache warm, over TLS from the HuggingFace hub, and (with pin mode on) are digest-checked before use.
998
+
999
+ **Monitoring policy:** run `npm audit` at each release and record the baseline. A *new* high-severity advisory in the transformers/onnxruntime chain requires a documented exception with rationale (release notes or a linked issue) — never silently dismiss; exceptions without rationale are release blockers.
1000
+
975
1001
  ## Architecture
976
1002
 
977
1003
  ### Three execution tiers
@@ -1102,7 +1128,7 @@ Contributors must run `npm run build` before publishing or consuming the library
1102
1128
  | `npm run verify:ci` | Full CI parity: build, typecheck, lint, test, coverage (baseline PR gate; see [PR and pre-release quality gate set](#pr-and-pre-release-quality-gate-set)) |
1103
1129
  | `npm run typecheck` | TypeScript strict mode check (`tsc --noEmit`) |
1104
1130
  | `npm test` | Run test suite (`vitest run`) |
1105
- | `npm run coverage:check` | Tests with line-coverage thresholds |
1131
+ | `npm run coverage:check` | Tests with line-coverage thresholds (src + extension, 80% floor — see [docs/extension-package-boundary.md](docs/extension-package-boundary.md#extension-coverage-gate-144)) |
1106
1132
  | `npm run lint` | ESLint + fleet catalog validation |
1107
1133
  | `npm run routing:bootstrap-centroids` | Regenerate `config/routing-centroids.json` from cluster catalog |
1108
1134
  | `npm run routing:calibration-aggregate` | Aggregate community telemetry for calibration |
@@ -7,8 +7,8 @@
7
7
  "livecodebench": "https://livecodebench.github.io/leaderboard.html",
8
8
  "bfcl": "https://gorilla.cs.berkeley.edu/leaderboard.html"
9
9
  },
10
- "scrape_date": "2026-09-03",
11
- "catalog_freeze_date": "2026-09-03"
10
+ "scrape_date": "2026-09-05",
11
+ "catalog_freeze_date": "2026-09-05"
12
12
  },
13
13
  "aliases": {
14
14
  "anthropic/claude-opus-4": "claude-opus-4-5",
@@ -0,0 +1,18 @@
1
+ {
2
+ "_documentation": {
3
+ "purpose": "SP-259 (#147) — SHA-256 digest pins for ONNX embedding artifacts loaded by src/domain/matching/embedding-provider.ts. Verified on load when pin mode is enabled; mismatch or missing pinned artifact fails closed.",
4
+ "activation": "Set SMART_ROUTER_ONNX_PIN_MODE=verify (verify configured pins; unpinned models still download) or =enforce (CI/prod — pins REQUIRED for the loaded model; missing pin file, missing pins, missing cached artifact, or any mismatch fails closed). Default is off (unpinned local dogfood preserved). Override this file path with SMART_ROUTER_ONNX_PIN_FILE.",
5
+ "format": "pins[modelId][cacheRelativePath] = lowercase hex SHA-256. Paths are relative to the model directory inside the transformers.js cache (hydra.artifact_cache_path): <cache>/<modelId>/<path> (hub-style models--org--name/snapshots/<rev>/ layout also supported).",
6
+ "digest_source": "Digests are HuggingFace LFS oids (= SHA-256 of file content) from https://huggingface.co/api/models/<modelId>/tree/main/onnx, fetched 2026-09-05. onnx/model_quantized.onnx is the default q8 artifact transformers.js loads for feature-extraction.",
7
+ "refresh": "Re-fetch digests from the HF API when upgrading model revisions; never pin digests you have not verified out-of-band."
8
+ },
9
+ "version": 1,
10
+ "pins": {
11
+ "Xenova/all-MiniLM-L6-v2": {
12
+ "onnx/model_quantized.onnx": "afdb6f1a0e45b715d0bb9b11772f032c399babd23bfc31fed1c170afc848bdb1"
13
+ },
14
+ "onnx-community/granite-embedding-97m-multilingual-r2-ONNX": {
15
+ "onnx/model_quantized.onnx": "704c1ebca5fbb7cd83ced41827658ac4c9990c64f7f2874d22b78044e5022e22"
16
+ }
17
+ }
18
+ }
@@ -22,6 +22,7 @@
22
22
  "artifact_cache_path": ".pi-smart-router/models/",
23
23
  "encoder": "minilm",
24
24
  "hydra_heads": "learned_projection",
25
+ "_onnx_artifact_pins_documentation": "SP-259 (#147) — ONNX artifact supply-chain pinning. Digests live in config/onnx-artifact-pins.json (SHA-256 per cached artifact). Enable with env SMART_ROUTER_ONNX_PIN_MODE=verify (verify configured pins; unpinned models still download) or =enforce (CI/prod: pins required for the loaded model — missing pin file, missing pins, missing cached artifact, or any digest mismatch fails closed). Default off preserves first-run unpinned dogfood downloads. Override pin file path with SMART_ROUTER_ONNX_PIN_FILE. See README 'HyDRA model cache → Supply-chain' for offline cache warm and audit posture.",
25
26
  "_hydra_heads_documentation": {
26
27
  "learned_projection": "Default — SP-115 384×3 linear projection on MiniLM/Granite embeddings (config/hydra-projection-weights.json). Approximates HyDRA K sigmoid heads; keep until calibration Top-1 error exceeds ~10%.",
27
28
  "modernbert_k4": "ModernBERT-base [CLS] with K=4 independent sigmoid heads (config/modernbert-k4-heads.json). Migrating from SP-115: train K=4 head weights offline, set hydra_heads to modernbert_k4, and remove or ignore hydra-projection-weights.json. The fourth debugging dimension is predicted for HyDRA fidelity but excluded from the catalog-decoupled shortfall gate (reasoning, code_gen, tool_use only). See docs/routing-roadmap.md §2 P3 and GitHub #81."
@@ -18,6 +18,43 @@ export interface TextEmbedder {
18
18
  embed(text: string): Promise<Float32Array>;
19
19
  dispose(): Promise<void>;
20
20
  }
21
+ /**
22
+ * Artifact pin verification mode:
23
+ * - `off` — no verification (default; preserves unpinned local dogfood downloads)
24
+ * - `verify` — verify cached artifacts when pins exist for the model; fail
25
+ * closed on digest mismatch or missing cached artifact. Models without pins
26
+ * remain unpinned (first-run download allowed).
27
+ * - `enforce` — CI/prod mode: pins are REQUIRED for the model; fail closed when
28
+ * the pin file is missing/unparseable, the model has no pins, a pinned
29
+ * artifact is missing from the cache, or any digest mismatches.
30
+ */
31
+ export type OnnxArtifactPinMode = 'off' | 'verify' | 'enforce';
32
+ export declare const ONNX_ARTIFACT_PIN_MODES: readonly OnnxArtifactPinMode[];
33
+ /** Default pin file (relative to process CWD). Override via options or env. */
34
+ export declare const DEFAULT_ONNX_PIN_FILE = "config/onnx-artifact-pins.json";
35
+ /** Env vars (operator wiring; schemas/hydra-matcher out of SP-259 scope). */
36
+ export declare const ONNX_PIN_MODE_ENV = "SMART_ROUTER_ONNX_PIN_MODE";
37
+ export declare const ONNX_PIN_FILE_ENV = "SMART_ROUTER_ONNX_PIN_FILE";
38
+ /** Pin file shape: per-model map of cache-relative file path → SHA-256 hex. */
39
+ export interface OnnxArtifactPinFile {
40
+ readonly version: number;
41
+ readonly pins: Record<string, Record<string, string>>;
42
+ }
43
+ export interface OnnxPinOptions {
44
+ /** Pin mode. Default: $SMART_ROUTER_ONNX_PIN_MODE, else 'off'. */
45
+ readonly pinMode?: OnnxArtifactPinMode;
46
+ /** Pin file path. Default: $SMART_ROUTER_ONNX_PIN_FILE, else DEFAULT_ONNX_PIN_FILE. */
47
+ readonly pinFilePath?: string;
48
+ }
49
+ /**
50
+ * Verify cached ONNX artifacts for `modelId` against configured SHA-256 pins.
51
+ * Fails closed on digest mismatch or missing pinned artifact; in `enforce`
52
+ * mode also fails when the pin file or model pins are missing.
53
+ *
54
+ * Runs after pipeline load so first-run downloads are verified before the
55
+ * embedder is returned; a mismatching session is never usable.
56
+ */
57
+ export declare function verifyOnnxArtifactPins(modelId: string, cachePath: string, options?: OnnxPinOptions): Promise<void>;
21
58
  /**
22
59
  * Creates a TextEmbedder backed by @huggingface/transformers ONNX runtime.
23
60
  * Model: Xenova/all-MiniLM-L6-v2 (384-dim).
@@ -25,12 +62,12 @@ export interface TextEmbedder {
25
62
  * The package is loaded dynamically — not required at compile time.
26
63
  * Install: `npm i @huggingface/transformers`
27
64
  */
28
- export declare function createOnnxTextEmbedder(artifactCachePath: string): Promise<TextEmbedder>;
65
+ export declare function createOnnxTextEmbedder(artifactCachePath: string, pinOptions?: OnnxPinOptions): Promise<TextEmbedder>;
29
66
  /**
30
67
  * Granite 97M long-context embedder (384-dim ONNX drop-in for SP-115 head).
31
68
  * Model: ibm-granite/granite-embedding-97m-multilingual-r2 via ONNX runtime.
32
69
  */
33
- export declare function createGraniteOnnxTextEmbedder(artifactCachePath: string): Promise<TextEmbedder>;
70
+ export declare function createGraniteOnnxTextEmbedder(artifactCachePath: string, pinOptions?: OnnxPinOptions): Promise<TextEmbedder>;
34
71
  /** Select ONNX text embedder by operator encoder flag. */
35
- export declare function createTextEmbedder(encoder: Encoder | undefined, artifactCachePath: string): Promise<TextEmbedder>;
72
+ export declare function createTextEmbedder(encoder: Encoder | undefined, artifactCachePath: string, pinOptions?: OnnxPinOptions): Promise<TextEmbedder>;
36
73
  //# sourceMappingURL=embedding-provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"embedding-provider.d.ts","sourceRoot":"","sources":["../../../src/domain/matching/embedding-provider.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAGnD,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,sDAAsD;AACtD,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAE3D;;;GAGG;AACH,eAAO,MAAM,kBAAkB,8DAC8B,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AA+DD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED;;;GAGG;AACH,wBAAsB,6BAA6B,CACjD,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,0DAA0D;AAC1D,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,YAAkB,EAClC,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,YAAY,CAAC,CAWvB"}
1
+ {"version":3,"file":"embedding-provider.d.ts","sourceRoot":"","sources":["../../../src/domain/matching/embedding-provider.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAGnD,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,sDAAsD;AACtD,eAAO,MAAM,iBAAiB,4BAA4B,CAAC;AAE3D;;;GAGG;AACH,eAAO,MAAM,kBAAkB,8DAC8B,CAAC;AAE9D,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAID;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE/D,eAAO,MAAM,uBAAuB,EAAE,SAAS,mBAAmB,EAIjE,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,mCAAmC,CAAC;AAEtE,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,+BAA+B,CAAC;AAC9D,eAAO,MAAM,iBAAiB,+BAA+B,CAAC;AAE9D,+EAA+E;AAC/E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IACvC,uFAAuF;IACvF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AA6FD;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAuCf;AA0GD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,iBAAiB,EAAE,MAAM,EACzB,UAAU,CAAC,EAAE,cAAc,GAC1B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED;;;GAGG;AACH,wBAAsB,6BAA6B,CACjD,iBAAiB,EAAE,MAAM,EACzB,UAAU,CAAC,EAAE,cAAc,GAC1B,OAAO,CAAC,YAAY,CAAC,CAEvB;AAED,0DAA0D;AAC1D,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,YAAkB,EAClC,iBAAiB,EAAE,MAAM,EACzB,UAAU,CAAC,EAAE,cAAc,GAC1B,OAAO,CAAC,YAAY,CAAC,CAWvB"}