pi-smart-router 0.20.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.
- package/.pi/extensions/smart-router/command-formatters.ts +9 -11
- package/.pi/extensions/smart-router/commands.ts +5 -2
- package/.pi/extensions/smart-router/dataset-export.ts +8 -7
- package/.pi/extensions/smart-router/delegate-stream.ts +8 -5
- package/.pi/extensions/smart-router/delegation-runtime.ts +8 -11
- package/.pi/extensions/smart-router/extension-setup.ts +9 -4
- package/.pi/extensions/smart-router/fleet-bootstrap.ts +19 -21
- package/.pi/extensions/smart-router/index.ts +5 -4
- package/.pi/extensions/smart-router/planning-delegate.ts +9 -8
- package/.pi/extensions/smart-router/pricing-lifecycle.ts +9 -4
- package/.pi/extensions/smart-router/route-and-delegate.ts +13 -12
- package/.pi/extensions/smart-router/routing-context.ts +4 -2
- package/.pi/extensions/smart-router/routing-outcomes.ts +6 -2
- package/.pi/extensions/smart-router/session-lifecycle.ts +63 -1
- package/.pi/extensions/smart-router/stream-delegation.ts +8 -2
- package/.pi/extensions/smart-router/types.ts +13 -16
- package/.pi/extensions/smart-router/utils.ts +5 -3
- package/README.md +46 -2
- package/config/benchmark-profiles.json +2 -3
- package/config/onnx-artifact-pins.json +18 -0
- package/config/operator-config.json.example +1 -0
- package/dist/api/middleware/pi-router-middleware.d.ts +8 -0
- package/dist/api/middleware/pi-router-middleware.d.ts.map +1 -1
- package/dist/api/middleware/pi-router-middleware.js +12 -0
- package/dist/api/middleware/pi-router-middleware.js.map +1 -1
- package/dist/api/session-eviction.d.ts +27 -0
- package/dist/api/session-eviction.d.ts.map +1 -0
- package/dist/api/session-eviction.js +21 -0
- package/dist/api/session-eviction.js.map +1 -0
- package/dist/cli/smart-router-cli.d.ts +9 -5
- package/dist/cli/smart-router-cli.d.ts.map +1 -1
- package/dist/cli/smart-router-cli.js +23 -16
- package/dist/cli/smart-router-cli.js.map +1 -1
- package/dist/domain/matching/embedding-provider.d.ts +40 -3
- package/dist/domain/matching/embedding-provider.d.ts.map +1 -1
- package/dist/domain/matching/embedding-provider.js +159 -9
- package/dist/domain/matching/embedding-provider.js.map +1 -1
- package/dist/domain/matching/hydra-matcher.d.ts +47 -0
- package/dist/domain/matching/hydra-matcher.d.ts.map +1 -1
- package/dist/domain/matching/hydra-matcher.js +53 -0
- package/dist/domain/matching/hydra-matcher.js.map +1 -1
- package/dist/domain/matching/missing-weights-reason-codes.d.ts +18 -0
- package/dist/domain/matching/missing-weights-reason-codes.d.ts.map +1 -0
- package/dist/domain/matching/missing-weights-reason-codes.js +17 -0
- package/dist/domain/matching/missing-weights-reason-codes.js.map +1 -0
- package/dist/domain/matching/modernbert-heads.d.ts +6 -0
- package/dist/domain/matching/modernbert-heads.d.ts.map +1 -1
- package/dist/domain/matching/modernbert-heads.js +4 -0
- package/dist/domain/matching/modernbert-heads.js.map +1 -1
- package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -1
- package/dist/domain/pipeline/router-pipeline.js +37 -2
- package/dist/domain/pipeline/router-pipeline.js.map +1 -1
- package/dist/domain/types/schemas.d.ts +2 -0
- package/dist/domain/types/schemas.d.ts.map +1 -1
- package/dist/domain/types/schemas.js +11 -0
- package/dist/domain/types/schemas.js.map +1 -1
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -1
- package/dist/infra/telemetry.d.ts +15 -7
- package/dist/infra/telemetry.d.ts.map +1 -1
- package/dist/infra/telemetry.js +33 -13
- package/dist/infra/telemetry.js.map +1 -1
- package/package.json +1 -1
- package/specs/001-build-smart-router/contracts/telemetry-contrib.schema.json +3 -3
- package/src/api/middleware/pi-router-middleware.ts +14 -0
- package/src/api/session-eviction.ts +36 -0
- package/src/cli/smart-router-cli.ts +23 -13
- package/src/domain/matching/embedding-provider.ts +241 -7
- package/src/domain/matching/hydra-matcher.ts +85 -0
- package/src/domain/matching/missing-weights-reason-codes.ts +22 -0
- package/src/domain/matching/modernbert-heads.ts +12 -0
- package/src/domain/pipeline/router-pipeline.ts +50 -1
- package/src/domain/types/schemas.ts +11 -0
- package/src/index.ts +153 -0
- package/src/infra/telemetry.ts +38 -9
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_HISTORY_LIMIT,
|
|
3
3
|
MAX_HISTORY_LIMIT,
|
|
4
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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 {
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
import type {
|
|
17
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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 {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
|
|
18
|
-
import {
|
|
14
|
+
SessionPinner,
|
|
19
15
|
collectPoolModelIds,
|
|
20
16
|
resolveQuotaWindowEstimateConfigFromEnv,
|
|
21
17
|
resolveQuotaWindowPosition,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
10
|
-
* from source at dev time and is excluded from
|
|
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/
|
|
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/
|
|
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/
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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/
|
|
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 {
|
|
2
|
-
|
|
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,6 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
evictInMemorySessionState,
|
|
5
|
+
SessionPinner,
|
|
6
|
+
} from '../../../src/index.js';
|
|
7
|
+
|
|
4
8
|
import {
|
|
5
9
|
bindSharedModelRegistry,
|
|
6
10
|
ensureFleetFresh,
|
|
@@ -15,6 +19,15 @@ export const FLEET_MODE_ENTRY_TYPE = 'smart-router-fleet-mode' as const;
|
|
|
15
19
|
const SMART_ROUTER_PROVIDER = 'smart-router' as const;
|
|
16
20
|
const SMART_ROUTER_AUTO_ID = 'auto' as const;
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Orphan-session TTL fallback (SP-248, #145). `session_shutdown` is the primary
|
|
24
|
+
* teardown signal; if pi ever shuts a session down without delivering it
|
|
25
|
+
* (extension reload races, missed events), the session's in-memory routing
|
|
26
|
+
* state is evicted once it has been idle for ORPHAN_SESSION_TTL_MS. The sweep
|
|
27
|
+
* runs on `session_start` and fails open — eviction must never break startup.
|
|
28
|
+
*/
|
|
29
|
+
export const ORPHAN_SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
30
|
+
|
|
18
31
|
export function isSmartRouterActive(model: { provider: string; id: string }): boolean {
|
|
19
32
|
return model.provider === SMART_ROUTER_PROVIDER && model.id === SMART_ROUTER_AUTO_ID;
|
|
20
33
|
}
|
|
@@ -97,6 +110,39 @@ export function setupSessionHooks(
|
|
|
97
110
|
): void {
|
|
98
111
|
let activeModel: { provider: string; id: string } | undefined;
|
|
99
112
|
|
|
113
|
+
// Last-seen stamp per session for the orphan-TTL fallback (SP-248).
|
|
114
|
+
const lastSeenBySession = new Map<string, number>();
|
|
115
|
+
|
|
116
|
+
/** Evict all in-memory routing state for one session via the SP-247 helper. */
|
|
117
|
+
function evictSessionState(sessionId: string | undefined): void {
|
|
118
|
+
// Fail open: a missing/empty session id must never break teardown.
|
|
119
|
+
if (sessionId === undefined || sessionId === '') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
evictInMemorySessionState(sessionId, {
|
|
123
|
+
executionLedger: runtime.executionLedger,
|
|
124
|
+
lifecycleHookState: runtime.lifecycleHookState,
|
|
125
|
+
sessionRouting: runtime.sessionRouting,
|
|
126
|
+
});
|
|
127
|
+
lastSeenBySession.delete(sessionId);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Drop sessions idle longer than ORPHAN_SESSION_TTL_MS (never throws). */
|
|
131
|
+
function sweepOrphanedSessions(now: number): void {
|
|
132
|
+
try {
|
|
133
|
+
for (const [sessionId, lastSeen] of lastSeenBySession) {
|
|
134
|
+
if (now - lastSeen > ORPHAN_SESSION_TTL_MS) {
|
|
135
|
+
evictSessionState(sessionId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.warn(
|
|
140
|
+
'[smart-router] orphan session sweep failed (fail open)',
|
|
141
|
+
error instanceof Error ? error.message : String(error),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
100
146
|
pi.on('session_start', async (_event, ctx) => {
|
|
101
147
|
activeModel = ctx.model;
|
|
102
148
|
bindSharedModelRegistry(runtime, ctx.modelRegistry);
|
|
@@ -128,6 +174,10 @@ export function setupSessionHooks(
|
|
|
128
174
|
datasetNotify.fn = runtime.notifyDatasetEnabled;
|
|
129
175
|
|
|
130
176
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
lastSeenBySession.set(sessionId, now);
|
|
179
|
+
sweepOrphanedSessions(now);
|
|
180
|
+
|
|
131
181
|
await sessionPinner.restoreSessionPin(sessionId);
|
|
132
182
|
|
|
133
183
|
if (ctx.model !== undefined && isSmartRouterActive(ctx.model)) {
|
|
@@ -158,5 +208,17 @@ export function setupSessionHooks(
|
|
|
158
208
|
delete runtime.streamDeps.ensureFleetFresh;
|
|
159
209
|
datasetNotify.fn = undefined;
|
|
160
210
|
ctx.ui.setStatus('smart-router-lmu', undefined);
|
|
211
|
+
|
|
212
|
+
// SP-248 (#145): drop per-session routing state so long-running pi
|
|
213
|
+
// processes do not retain ledger/lifecycle/routing maps forever.
|
|
214
|
+
// Fail open — teardown cleanup must never crash the host agent.
|
|
215
|
+
try {
|
|
216
|
+
evictSessionState(ctx.sessionManager.getSessionId());
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.warn(
|
|
219
|
+
'[smart-router] session teardown eviction failed (fail open)',
|
|
220
|
+
error instanceof Error ? error.message : String(error),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
161
223
|
});
|
|
162
224
|
}
|
|
@@ -7,8 +7,14 @@ import {
|
|
|
7
7
|
createAssistantMessageEventStream,
|
|
8
8
|
} from '@earendil-works/pi-ai/compat';
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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/
|
|
8
|
-
import type {
|
|
9
|
-
|
|
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)
|
|
@@ -489,6 +489,8 @@ v0.2.0 adds **Session-Aware Agentic Routing (SAAR)** pin knobs ([#72](https://gi
|
|
|
489
489
|
|
|
490
490
|
See [routing-roadmap.md](docs/routing-roadmap.md) §2 P0 for design context.
|
|
491
491
|
|
|
492
|
+
**Long-running pi sessions — in-memory state eviction (v0.21.0, [#145](https://github.com/beettlle/pi-smart-router/issues/145)).** Session pins and in-memory routing snapshots live in process memory, not in the SQLite telemetry store. When pi ends a session, the extension's `session_shutdown` handler (reason: `quit` / `reload` / `new` / `resume` / `fork`) calls `evictInMemorySessionState` (`src/api/session-eviction.ts`) and drops **all** in-memory routing state for that session — pins, cache-breakeven snapshots, turn metadata. A new session starts cold: no stale pin, no warm-cache assumption carries over. Persistent telemetry in `.pi-smart-router/state.db` is untouched. As a safety net for orphaned sessions (e.g. a crashed pi process that never emitted `session_shutdown`), a sweep on `session_start` evicts sessions idle longer than `ORPHAN_SESSION_TTL_MS` (**24 hours**, exported from `.pi/extensions/smart-router/session-lifecycle.ts`); the sweep fails open — a missing session id or sweep error never blocks session start.
|
|
493
|
+
|
|
492
494
|
### Planning delegate (v0.4.0 Delegate)
|
|
493
495
|
|
|
494
496
|
When a **planning** turn would route primary inference to frontier while a warm **economical** session pin is active, smart-router prefers **cache-preserving delegation** ([#71](https://github.com/beettlle/pi-smart-router/issues/71)):
|
|
@@ -570,6 +572,16 @@ Explain/telemetry expose `route_path` (`neural` \| `learned` \| `heuristic` \| `
|
|
|
570
572
|
| `learned_min_confidence` | `0.6` | Minimum learned-entry confidence to honor a tier suggestion |
|
|
571
573
|
| `learned_max_entries` | `512` | Learned-map cap per key space (FIFO eviction) |
|
|
572
574
|
| `pattern_tool_use_ceiling` | `0.3` | Tool-use cue ceiling for honoring cheaper-tier learned/pattern suggestions |
|
|
575
|
+
| `fail_closed_on_missing_weights` | `false` | When `true`, missing/placeholder neural weights fail **closed**: the matcher throws before paying embedding cost and the pipeline routes through the degraded sandwich (`neural_misconfigured`) with the SP-251 reason codes on the decision. When `false` (default), routing stays fail-open — placeholder weights score with neutral defaults and the reason codes are advisory telemetry only |
|
|
576
|
+
|
|
577
|
+
**Missing-weights reason codes (v0.21.0, [#148](https://github.com/beettlle/pi-smart-router/issues/148)).** When the HyDRA matcher cannot load real neural weights, the decision surfaces structured reason codes (`src/domain/matching/missing-weights-reason-codes.ts`) on the routing decision / `requirement_reason_codes` — visible in explain (`pi router explain` / `POST /v1/route/explain`), `/smart-router history`, and telemetry — not stderr-only:
|
|
578
|
+
|
|
579
|
+
| Reason code | Meaning | Operator action |
|
|
580
|
+
|-------------|---------|-----------------|
|
|
581
|
+
| `hydra_weights_missing` | HyDRA projection-head weights artifact is absent or unloadable, so neural scoring is running on fallback behavior | Restore the weights artifact (see [HyDRA model cache](#hydra-model-cache)); routing continues fail-open unless `fail_closed_on_missing_weights` is set |
|
|
582
|
+
| `k4_heads_placeholder` | ModernBERT K4 heads are placeholder (untrained) weights — scores are neutral defaults, not learned predictions | Regenerate/install the trained K4 heads artifact; treat current K4 scores as non-authoritative |
|
|
583
|
+
|
|
584
|
+
With the default fail-open behavior these codes are advisory: the route proceeds with safe defaults. Set `degraded_route.fail_closed_on_missing_weights: true` when you prefer an explicit degraded-route decision (sandwich chain above, `route_path` records the branch taken) over silently routing on placeholder neural scores.
|
|
573
585
|
|
|
574
586
|
Distinct from soft heat affinity (healthy-path bias): this is failover / skip-expensive-stage only. Routing remains **pre-generation** — no FrugalGPT-style cascades (see [routing-roadmap.md](docs/routing-roadmap.md) §1).
|
|
575
587
|
|
|
@@ -764,6 +776,12 @@ npx pi-smart-router export telemetry-contrib [--limit N]
|
|
|
764
776
|
|
|
765
777
|
This writes schema-valid JSON to `.pi-smart-router/exports/telemetry-contrib-<timestamp>.json`. Each row conforms to [`telemetry-contrib.schema.json`](specs/001-build-smart-router/contracts/telemetry-contrib.schema.json).
|
|
766
778
|
|
|
779
|
+
**Export schema v2 — session-hash migration (v0.21.0, [#146](https://github.com/beettlle/pi-smart-router/issues/146)).** The contrib export schema was bumped **v1 → v2** (`TELEMETRY_CONTRIB_VERSION = 2` in `src/cli/smart-router-cli.ts`). In v1, `session_id_hash` was an **unsalted SHA-256** of the raw session id; in v2 it is an **HMAC-SHA256 keyed with an install-local pepper** (`hashSessionIdForTelemetryExport` in `src/infra/telemetry.ts`). The pepper is generated once per install at `.pi-smart-router/.dataset-key` (mode 0600) and is **never** included in export payloads — ingest strips pepper fields before aggregation. Consequences for operators comparing exports:
|
|
780
|
+
|
|
781
|
+
- Hashes are **stable per install** (same session → same hash within one install/cwd) but **not correlatable across installs** — two machines routing the same prompt produce different hashes.
|
|
782
|
+
- **v1 hashes are not comparable with v2 hashes.** If you maintain baselines that join or diff older v1 contrib exports, **re-baseline** on v2 output — do not mix rows across the version boundary.
|
|
783
|
+
- Raw `session_id` / `request_id` values never appear in the JSONL in either version.
|
|
784
|
+
|
|
767
785
|
**How to contribute**
|
|
768
786
|
|
|
769
787
|
1. Opt in to dataset capture (`SMART_ROUTER_DATASET=1`) and dogfood with `/model smart-router/auto` for several sessions.
|
|
@@ -954,6 +972,32 @@ npm run benchmark:encoder
|
|
|
954
972
|
|
|
955
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.
|
|
956
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
|
+
|
|
957
1001
|
## Architecture
|
|
958
1002
|
|
|
959
1003
|
### Three execution tiers
|
|
@@ -1084,7 +1128,7 @@ Contributors must run `npm run build` before publishing or consuming the library
|
|
|
1084
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)) |
|
|
1085
1129
|
| `npm run typecheck` | TypeScript strict mode check (`tsc --noEmit`) |
|
|
1086
1130
|
| `npm test` | Run test suite (`vitest run`) |
|
|
1087
|
-
| `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)) |
|
|
1088
1132
|
| `npm run lint` | ESLint + fleet catalog validation |
|
|
1089
1133
|
| `npm run routing:bootstrap-centroids` | Regenerate `config/routing-centroids.json` from cluster catalog |
|
|
1090
1134
|
| `npm run routing:calibration-aggregate` | Aggregate community telemetry for calibration |
|