pi-smart-router 0.19.4 → 0.21.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 +19 -3
- package/.pi/extensions/smart-router/delegate-stream.ts +78 -1
- package/.pi/extensions/smart-router/delegation-runtime.ts +20 -4
- package/.pi/extensions/smart-router/extension-setup.ts +29 -0
- package/.pi/extensions/smart-router/planning-delegate.ts +23 -0
- package/.pi/extensions/smart-router/route-and-delegate.ts +25 -0
- package/.pi/extensions/smart-router/session-lifecycle.ts +59 -0
- package/.pi/extensions/smart-router/stream-delegation.ts +15 -0
- package/.pi/extensions/smart-router/types.ts +17 -0
- package/README.md +100 -1
- package/config/benchmark-profiles.json +2 -3
- package/config/operator-config.json.example +10 -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/config/defaults.d.ts +2 -2
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +6 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/pi-model-mapper.d.ts.map +1 -1
- package/dist/config/pi-model-mapper.js +5 -0
- package/dist/config/pi-model-mapper.js.map +1 -1
- package/dist/domain/delegation/adaptive-reasoning.d.ts +114 -0
- package/dist/domain/delegation/adaptive-reasoning.d.ts.map +1 -0
- package/dist/domain/delegation/adaptive-reasoning.js +273 -0
- package/dist/domain/delegation/adaptive-reasoning.js.map +1 -0
- 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/pricing/peak-pricing.d.ts +94 -0
- package/dist/domain/pricing/peak-pricing.d.ts.map +1 -0
- package/dist/domain/pricing/peak-pricing.js +144 -0
- package/dist/domain/pricing/peak-pricing.js.map +1 -0
- package/dist/domain/routing/expected-cost.d.ts +89 -0
- package/dist/domain/routing/expected-cost.d.ts.map +1 -1
- package/dist/domain/routing/expected-cost.js +138 -2
- package/dist/domain/routing/expected-cost.js.map +1 -1
- package/dist/domain/types/entities.d.ts +39 -0
- package/dist/domain/types/entities.d.ts.map +1 -1
- package/dist/domain/types/index.d.ts +2 -1
- package/dist/domain/types/index.d.ts.map +1 -1
- package/dist/domain/types/schemas.d.ts +66 -0
- package/dist/domain/types/schemas.d.ts.map +1 -1
- package/dist/domain/types/schemas.js +95 -0
- package/dist/domain/types/schemas.js.map +1 -1
- package/dist/domain/types/store-port.d.ts +16 -1
- package/dist/domain/types/store-port.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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/dist/infrastructure/persistence/sqlite-store.d.ts +16 -1
- package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -1
- package/dist/infrastructure/persistence/sqlite-store.js +119 -4
- package/dist/infrastructure/persistence/sqlite-store.js.map +1 -1
- package/dist/infrastructure/pricing/price-broker.d.ts +7 -1
- package/dist/infrastructure/pricing/price-broker.d.ts.map +1 -1
- package/dist/infrastructure/pricing/price-broker.js +11 -5
- package/dist/infrastructure/pricing/price-broker.js.map +1 -1
- package/dist/infrastructure/telemetry/routing-telemetry.d.ts +69 -4
- package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -1
- package/dist/infrastructure/telemetry/routing-telemetry.js +132 -3
- package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -1
- package/dist/infrastructure/telemetry/session-stats.d.ts +49 -1
- package/dist/infrastructure/telemetry/session-stats.d.ts.map +1 -1
- package/dist/infrastructure/telemetry/session-stats.js +78 -6
- package/dist/infrastructure/telemetry/session-stats.js.map +1 -1
- package/package.json +2 -1
- package/skills/router-release-operator/SKILL.md +67 -12
- package/skills/router-release-operator/references/issue-intake-checklist.md +21 -8
- package/skills/router-release-operator/references/release-manifest-template.md +18 -7
- package/skills/router-release-operator/references/release-profiles.md +13 -0
- 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/config/defaults.ts +8 -1
- package/src/config/pi-model-mapper.ts +5 -0
- package/src/domain/delegation/adaptive-reasoning.ts +407 -0
- 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/pricing/peak-pricing.ts +220 -0
- package/src/domain/routing/expected-cost.ts +240 -2
- package/src/domain/types/entities.ts +43 -0
- package/src/domain/types/index.ts +7 -0
- package/src/domain/types/schemas.ts +112 -0
- package/src/domain/types/store-port.ts +18 -1
- package/src/index.ts +2 -0
- package/src/infra/telemetry.ts +38 -9
- package/src/infrastructure/persistence/sqlite-store.ts +138 -5
- package/src/infrastructure/pricing/price-broker.ts +15 -4
- package/src/infrastructure/telemetry/routing-telemetry.ts +186 -6
- package/src/infrastructure/telemetry/session-stats.ts +126 -7
|
@@ -312,9 +312,16 @@ export function formatStatsMessage(
|
|
|
312
312
|
return 'No routing stats yet (empty telemetry window).';
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
const costBasisLabel =
|
|
316
|
+
snapshot.cost_basis === 'actual'
|
|
317
|
+
? 'actual'
|
|
318
|
+
: snapshot.cost_basis === 'mixed'
|
|
319
|
+
? `mixed (${snapshot.actual_usage_count}/${snapshot.entry_count} actual)`
|
|
320
|
+
: 'estimated';
|
|
321
|
+
|
|
315
322
|
const lines = [
|
|
316
323
|
`Entries: ${snapshot.entry_count}`,
|
|
317
|
-
`Cost: total ${formatUsd(snapshot.total_cost_usd)} | mean ${formatMean(snapshot.mean_cost_usd, '')}`,
|
|
324
|
+
`Cost (${costBasisLabel}): total ${formatUsd(snapshot.total_cost_usd)} | mean ${formatMean(snapshot.mean_cost_usd, '')}`,
|
|
318
325
|
`Latency: total ${snapshot.total_latency_ms.toFixed(0)}ms | mean ${formatMean(snapshot.mean_latency_ms, 'ms')}`,
|
|
319
326
|
`Planning delegate share: ${formatShare(snapshot.planning_delegate_share)} (direct ${formatShare(snapshot.direct_share)})`,
|
|
320
327
|
`Local vs cloud (when known): local ${formatShare(snapshot.local_share)} | cloud ${formatShare(snapshot.cloud_share)}`,
|
|
@@ -324,10 +331,19 @@ export function formatStatsMessage(
|
|
|
324
331
|
` other: ${snapshot.role_cost.other.count} | ${formatUsd(snapshot.role_cost.other.total_cost_usd)}`,
|
|
325
332
|
];
|
|
326
333
|
|
|
334
|
+
if (snapshot.actual_usage_count > 0) {
|
|
335
|
+
lines.push(
|
|
336
|
+
`Actual usage: ${snapshot.actual_usage_count}/${snapshot.entry_count} entries with host-reported tokens` +
|
|
337
|
+
(snapshot.actual_total_tokens !== null
|
|
338
|
+
? ` | ${snapshot.actual_total_tokens} actual tokens`
|
|
339
|
+
: ''),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
327
343
|
if (snapshot.frontier_savings_usd !== undefined) {
|
|
328
344
|
lines.push(
|
|
329
|
-
`Vs always-frontier savings
|
|
330
|
-
' formula: sum max(0, tokens/1e6 * frontier_cost_per_1m -
|
|
345
|
+
`Vs always-frontier savings: ${formatUsd(snapshot.frontier_savings_usd)} (${snapshot.cost_basis === 'estimated' ? 'est.' : 'actuals preferred, est. when missing'})`,
|
|
346
|
+
' formula: sum max(0, tokens/1e6 * frontier_cost_per_1m - cost); tokens/cost prefer actuals; omitted when prices missing',
|
|
331
347
|
);
|
|
332
348
|
} else {
|
|
333
349
|
lines.push('Vs always-frontier savings: (omitted — frontier prices unavailable)');
|
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
} from '@earendil-works/pi-ai/compat';
|
|
11
11
|
|
|
12
12
|
import { parseAssistantMessageError } from '../../../src/infrastructure/delegation/provider-error.js';
|
|
13
|
+
import { extractUsageActuals } from '../../../src/infrastructure/telemetry/routing-telemetry.js';
|
|
14
|
+
import {
|
|
15
|
+
resolveAdaptiveReasoning,
|
|
16
|
+
type AdaptiveReasoningResult,
|
|
17
|
+
type AdaptiveReasoningSignal,
|
|
18
|
+
} from '../../../src/domain/delegation/adaptive-reasoning.js';
|
|
13
19
|
import {
|
|
14
20
|
buildDelegationContext,
|
|
15
21
|
forwardDelegatedEvent,
|
|
@@ -89,6 +95,7 @@ export async function collectDelegatedStream(
|
|
|
89
95
|
deps: StreamDelegationDeps,
|
|
90
96
|
options: SimpleStreamOptions | undefined,
|
|
91
97
|
headroomContext?: DelegationHeadroomContext,
|
|
98
|
+
reasoning?: AdaptiveReasoningResult,
|
|
92
99
|
): Promise<DelegatedStreamResult> {
|
|
93
100
|
throwIfAborted(options);
|
|
94
101
|
|
|
@@ -97,6 +104,7 @@ export async function collectDelegatedStream(
|
|
|
97
104
|
targetModel,
|
|
98
105
|
options,
|
|
99
106
|
headroomContext,
|
|
107
|
+
reasoning,
|
|
100
108
|
);
|
|
101
109
|
const delegateStream = resolveDelegateStream(targetModel, deps);
|
|
102
110
|
const inner = delegateStream(targetModel, context, delegationOptions);
|
|
@@ -156,6 +164,7 @@ export async function pipeDelegatedStream(
|
|
|
156
164
|
options: SimpleStreamOptions | undefined,
|
|
157
165
|
headroomContext: DelegationHeadroomContext | undefined,
|
|
158
166
|
pipe: PipeDelegatedStreamOptions,
|
|
167
|
+
reasoning?: AdaptiveReasoningResult,
|
|
159
168
|
): Promise<PipedDelegatedStreamResult> {
|
|
160
169
|
throwIfAborted(options);
|
|
161
170
|
|
|
@@ -164,6 +173,7 @@ export async function pipeDelegatedStream(
|
|
|
164
173
|
targetModel,
|
|
165
174
|
options,
|
|
166
175
|
headroomContext,
|
|
176
|
+
reasoning,
|
|
167
177
|
);
|
|
168
178
|
const delegateStream = resolveDelegateStream(targetModel, deps);
|
|
169
179
|
const inner = delegateStream(targetModel, context, delegationOptions);
|
|
@@ -257,11 +267,29 @@ function recordDelegateOutcome(
|
|
|
257
267
|
deps: StreamDelegationDeps,
|
|
258
268
|
sessionId: string | undefined,
|
|
259
269
|
result: DelegatedStreamResult,
|
|
270
|
+
requestId?: string,
|
|
260
271
|
): void {
|
|
261
272
|
if (!result.finalMessage) {
|
|
262
273
|
return;
|
|
263
274
|
}
|
|
264
275
|
|
|
276
|
+
// SP-241 / #164: capture post-turn usage actuals (success and
|
|
277
|
+
// failed-with-usage terminals). Fail open — extraction/hook errors and
|
|
278
|
+
// missing usage must never fail the route.
|
|
279
|
+
if (requestId !== undefined) {
|
|
280
|
+
try {
|
|
281
|
+
const actuals = extractUsageActuals(result.finalMessage.usage);
|
|
282
|
+
if (actuals) {
|
|
283
|
+
deps.onDelegationUsage?.(requestId, actuals);
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.warn(
|
|
287
|
+
'[smart-router] usage actuals capture failed (fail open)',
|
|
288
|
+
error instanceof Error ? error.message : String(error),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
265
293
|
if (result.failed) {
|
|
266
294
|
const parsed = parseAssistantMessageError(result.finalMessage);
|
|
267
295
|
deps.router.dispatch.recordOutcome(targetModel.id, parsed);
|
|
@@ -282,6 +310,11 @@ function recordDelegateOutcome(
|
|
|
282
310
|
/**
|
|
283
311
|
* Delegate with outcome recording. When `pipe` is provided, live-forwards to outer
|
|
284
312
|
* (holding the terminal event). Otherwise collects into a buffer (planning / probes).
|
|
313
|
+
*
|
|
314
|
+
* SP-245 (#166): when `reasoningSignal` is provided, the adaptive reasoning
|
|
315
|
+
* policy resolves the effective thinking level from turn signals and merges it
|
|
316
|
+
* into the delegated stream options (never lowering an explicit operator
|
|
317
|
+
* /thinking; fail open on non-reasoning models).
|
|
285
318
|
*/
|
|
286
319
|
export async function delegateWithOutcome(
|
|
287
320
|
targetModel: Model<Api>,
|
|
@@ -291,12 +324,54 @@ export async function delegateWithOutcome(
|
|
|
291
324
|
sessionId: string | undefined,
|
|
292
325
|
headroomContext?: DelegationHeadroomContext,
|
|
293
326
|
pipe?: PipeDelegatedStreamOptions,
|
|
327
|
+
/** Routing request id for post-turn usage actuals capture (SP-241, #164). */
|
|
328
|
+
requestId?: string,
|
|
329
|
+
/** Turn/routing signals for adaptive reasoning (SP-245, #166). */
|
|
330
|
+
reasoningSignal?: AdaptiveReasoningSignal,
|
|
294
331
|
): Promise<PipedDelegatedStreamResult | DelegatedStreamResult> {
|
|
332
|
+
const reasoning = reasoningSignal
|
|
333
|
+
? resolveAdaptiveReasoning(
|
|
334
|
+
targetModel,
|
|
335
|
+
reasoningSignal,
|
|
336
|
+
options?.reasoning,
|
|
337
|
+
// SP-246 (#166): operator enable/disable + floor/ceiling knobs.
|
|
338
|
+
deps.adaptiveReasoningConfig
|
|
339
|
+
? {
|
|
340
|
+
enabled: deps.adaptiveReasoningConfig.enabled,
|
|
341
|
+
...(deps.adaptiveReasoningConfig.min_level !== undefined
|
|
342
|
+
? { floor: deps.adaptiveReasoningConfig.min_level }
|
|
343
|
+
: {}),
|
|
344
|
+
...(deps.adaptiveReasoningConfig.max_level !== undefined
|
|
345
|
+
? { ceiling: deps.adaptiveReasoningConfig.max_level }
|
|
346
|
+
: {}),
|
|
347
|
+
}
|
|
348
|
+
: undefined,
|
|
349
|
+
)
|
|
350
|
+
: undefined;
|
|
351
|
+
|
|
352
|
+
// SP-246 (#166): reasoning telemetry (requested / applied / reason code).
|
|
353
|
+
// Fail open — telemetry must never fail the route.
|
|
354
|
+
if (reasoningSignal && reasoning && requestId !== undefined) {
|
|
355
|
+
try {
|
|
356
|
+
deps.onDelegationReasoning?.(requestId, {
|
|
357
|
+
reasoning_level_requested: options?.reasoning ?? null,
|
|
358
|
+
reasoning_level_applied: reasoning.reasoning ?? null,
|
|
359
|
+
reasoning_reason_code: reasoning.reasonCode,
|
|
360
|
+
});
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.warn(
|
|
363
|
+
'[smart-router] reasoning telemetry callback failed (fail open)',
|
|
364
|
+
error instanceof Error ? error.message : String(error),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
295
369
|
const delegationContext = buildDelegationContext(
|
|
296
370
|
context,
|
|
297
371
|
targetModel,
|
|
298
372
|
deps,
|
|
299
373
|
sessionId,
|
|
374
|
+
reasoning,
|
|
300
375
|
);
|
|
301
376
|
|
|
302
377
|
const result = pipe
|
|
@@ -307,6 +382,7 @@ export async function delegateWithOutcome(
|
|
|
307
382
|
options,
|
|
308
383
|
headroomContext,
|
|
309
384
|
pipe,
|
|
385
|
+
reasoning,
|
|
310
386
|
)
|
|
311
387
|
: await collectDelegatedStream(
|
|
312
388
|
targetModel,
|
|
@@ -314,9 +390,10 @@ export async function delegateWithOutcome(
|
|
|
314
390
|
deps,
|
|
315
391
|
options,
|
|
316
392
|
headroomContext,
|
|
393
|
+
reasoning,
|
|
317
394
|
);
|
|
318
395
|
|
|
319
|
-
recordDelegateOutcome(targetModel, deps, sessionId, result);
|
|
396
|
+
recordDelegateOutcome(targetModel, deps, sessionId, result, requestId);
|
|
320
397
|
|
|
321
398
|
return result;
|
|
322
399
|
}
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
} from '@earendil-works/pi-ai/compat';
|
|
14
14
|
import type { ModelRegistry } from '@earendil-works/pi-coding-agent';
|
|
15
15
|
|
|
16
|
+
import {
|
|
17
|
+
applyConcisenessHint,
|
|
18
|
+
type AdaptiveReasoningResult,
|
|
19
|
+
} from '../../../src/domain/delegation/adaptive-reasoning.js';
|
|
16
20
|
import {
|
|
17
21
|
isGoogleDelegationTarget,
|
|
18
22
|
normalizeDelegationContext,
|
|
@@ -87,6 +91,7 @@ export async function resolveDelegationOptions(
|
|
|
87
91
|
targetModel: Model<Api>,
|
|
88
92
|
callerOptions?: SimpleStreamOptions,
|
|
89
93
|
headroomContext?: DelegationHeadroomContext,
|
|
94
|
+
reasoning?: AdaptiveReasoningResult,
|
|
90
95
|
): Promise<SimpleStreamOptions> {
|
|
91
96
|
const auth = await modelRegistry.getApiKeyAndHeaders(targetModel);
|
|
92
97
|
if (!auth.ok) {
|
|
@@ -124,6 +129,14 @@ export async function resolveDelegationOptions(
|
|
|
124
129
|
...(auth.headers !== undefined ? { headers: auth.headers } : {}),
|
|
125
130
|
...(mergedEnv !== undefined ? { env: mergedEnv } : {}),
|
|
126
131
|
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
132
|
+
// SP-245 (#166): adaptive reasoning policy merge — applied after the
|
|
133
|
+
// caller pick so the policy-effective level wins, while every other
|
|
134
|
+
// DELEGATION_CALLER_OPTION_KEYS entry (incl. thinkingBudgets) passes
|
|
135
|
+
// through untouched. Explicit operator /thinking floors are already
|
|
136
|
+
// resolved inside the policy result.
|
|
137
|
+
...(reasoning?.reasoning !== undefined
|
|
138
|
+
? { reasoning: reasoning.reasoning }
|
|
139
|
+
: {}),
|
|
127
140
|
};
|
|
128
141
|
}
|
|
129
142
|
|
|
@@ -146,6 +159,7 @@ export function buildDelegationContext(
|
|
|
146
159
|
targetModel: Model<Api>,
|
|
147
160
|
deps: StreamDelegationDeps,
|
|
148
161
|
sessionId: string | undefined,
|
|
162
|
+
reasoning?: AdaptiveReasoningResult,
|
|
149
163
|
): Context {
|
|
150
164
|
const sessionExecution = sessionId
|
|
151
165
|
? deps.executionLedger.getLastExecution(sessionId)
|
|
@@ -155,11 +169,13 @@ export function buildDelegationContext(
|
|
|
155
169
|
sessionExecution,
|
|
156
170
|
});
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
172
|
+
const repaired = isGoogleDelegationTarget(targetModel)
|
|
173
|
+
? repairGeminiReplayContext(normalized, targetModel, sessionExecution)
|
|
174
|
+
: normalized;
|
|
161
175
|
|
|
162
|
-
|
|
176
|
+
// SP-245 (#166): light conciseness nudge only when the policy resolved a
|
|
177
|
+
// low/minimal effective level on a high-verbosity (GLM-class) profile.
|
|
178
|
+
return applyConcisenessHint(repaired, reasoning);
|
|
163
179
|
}
|
|
164
180
|
|
|
165
181
|
export function createErrorMessage(
|
|
@@ -120,6 +120,9 @@ export async function createSmartRouterRuntime(cwd: string): Promise<{
|
|
|
120
120
|
executionLedger,
|
|
121
121
|
lifecycleHookState,
|
|
122
122
|
planningDelegateConfig: operatorConfig.planning_delegate,
|
|
123
|
+
...(operatorConfig.adaptive_reasoning !== undefined
|
|
124
|
+
? { adaptiveReasoningConfig: operatorConfig.adaptive_reasoning }
|
|
125
|
+
: {}),
|
|
123
126
|
datasetRecorder,
|
|
124
127
|
outcomeRecorder,
|
|
125
128
|
sessionPinner,
|
|
@@ -138,6 +141,32 @@ export async function createSmartRouterRuntime(cwd: string): Promise<{
|
|
|
138
141
|
}
|
|
139
142
|
runtime.syncRegisteredLimits?.(limits);
|
|
140
143
|
},
|
|
144
|
+
onDelegationUsage(requestId, actuals) {
|
|
145
|
+
// SP-241 / #164: persist post-turn usage actuals onto the routing
|
|
146
|
+
// telemetry row. Fail open — a telemetry write must never fail the
|
|
147
|
+
// route, and stores without update support simply skip actuals.
|
|
148
|
+
try {
|
|
149
|
+
runtime.store.updateTelemetryUsageActuals?.(requestId, actuals);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.warn(
|
|
152
|
+
'[smart-router] failed to persist usage actuals (fail open)',
|
|
153
|
+
error instanceof Error ? error.message : String(error),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
onDelegationReasoning(requestId, fields) {
|
|
158
|
+
// SP-246 / #166: persist the adaptive reasoning decision (requested /
|
|
159
|
+
// applied level + reason code) onto the routing telemetry row. Fail
|
|
160
|
+
// open — telemetry must never fail the route.
|
|
161
|
+
try {
|
|
162
|
+
runtime.store.updateTelemetryReasoning?.(requestId, fields);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.warn(
|
|
165
|
+
'[smart-router] failed to persist reasoning telemetry (fail open)',
|
|
166
|
+
error instanceof Error ? error.message : String(error),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
141
170
|
},
|
|
142
171
|
};
|
|
143
172
|
|
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
PlanningDelegateObservability,
|
|
28
28
|
RoutingDecision,
|
|
29
29
|
} from '../../../src/domain/types/index.js';
|
|
30
|
+
import { resolveAdaptiveReasoning } from '../../../src/domain/delegation/adaptive-reasoning.js';
|
|
30
31
|
import { DEFAULT_PLANNING_DELEGATE_CONFIG } from '../../../src/domain/types/schemas.js';
|
|
31
32
|
import {
|
|
32
33
|
createPlanningDelegateObservability,
|
|
@@ -190,11 +191,33 @@ export async function defaultSpawnPlanningDelegate(
|
|
|
190
191
|
deps: StreamDelegationDeps,
|
|
191
192
|
): Promise<PlanningDelegateSpawnResult> {
|
|
192
193
|
try {
|
|
194
|
+
// SP-245 (#166): planning sub-calls keep higher reasoning — the adaptive
|
|
195
|
+
// policy resolves at least `medium` for planning turns (explicit operator
|
|
196
|
+
// /thinking floors still win inside the policy). SP-246: honor the
|
|
197
|
+
// operator enable/disable + floor/ceiling knobs.
|
|
198
|
+
const reasoning = resolveAdaptiveReasoning(
|
|
199
|
+
frontierModel,
|
|
200
|
+
{ turnType: 'planning' },
|
|
201
|
+
options?.reasoning,
|
|
202
|
+
deps.adaptiveReasoningConfig
|
|
203
|
+
? {
|
|
204
|
+
enabled: deps.adaptiveReasoningConfig.enabled,
|
|
205
|
+
...(deps.adaptiveReasoningConfig.min_level !== undefined
|
|
206
|
+
? { floor: deps.adaptiveReasoningConfig.min_level }
|
|
207
|
+
: {}),
|
|
208
|
+
...(deps.adaptiveReasoningConfig.max_level !== undefined
|
|
209
|
+
? { ceiling: deps.adaptiveReasoningConfig.max_level }
|
|
210
|
+
: {}),
|
|
211
|
+
}
|
|
212
|
+
: undefined,
|
|
213
|
+
);
|
|
193
214
|
const result = await collectDelegatedStream(
|
|
194
215
|
frontierModel,
|
|
195
216
|
compressedContext,
|
|
196
217
|
deps,
|
|
197
218
|
options,
|
|
219
|
+
undefined,
|
|
220
|
+
reasoning,
|
|
198
221
|
);
|
|
199
222
|
if (result.failed || !result.finalMessage) {
|
|
200
223
|
return {
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
|
|
10
10
|
import { safeCloudDefault } from '../../../src/domain/pipeline/safe-default.js';
|
|
11
11
|
import { computeOutputHeadroom } from '../../../src/domain/delegation/output-headroom.js';
|
|
12
|
+
import { resolvePeakPricingAdjustment } from '../../../src/domain/pricing/peak-pricing.js';
|
|
12
13
|
import {
|
|
13
14
|
CONTEXT_OVERFLOW_NO_FIT,
|
|
14
15
|
resolveContextOverflowFallback,
|
|
@@ -84,6 +85,14 @@ function logRoutingDecision(
|
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// SP-244 / #165: surface peak vs off-peak pricing rationale for the selected
|
|
89
|
+
// model (Z.ai GLM Coding Plan / DeepSeek API windows; multiplier 1 + window
|
|
90
|
+
// 'none' for non-target providers — fail open).
|
|
91
|
+
const peakPricing = resolvePeakPricingAdjustment({
|
|
92
|
+
id: delegate?.modelId ?? decision.selected_model_id,
|
|
93
|
+
...(delegate?.provider !== undefined ? { provider: delegate.provider } : {}),
|
|
94
|
+
});
|
|
95
|
+
|
|
87
96
|
console.warn(
|
|
88
97
|
'[smart-router] routing decision',
|
|
89
98
|
JSON.stringify({
|
|
@@ -93,6 +102,12 @@ function logRoutingDecision(
|
|
|
93
102
|
stage: decision.stage,
|
|
94
103
|
reason_code: decision.reason_code,
|
|
95
104
|
routing_latency_ms: decision.routing_latency_ms,
|
|
105
|
+
pricing_window: peakPricing.window,
|
|
106
|
+
peak_pricing: {
|
|
107
|
+
window: peakPricing.window,
|
|
108
|
+
cost_multiplier: peakPricing.cost_multiplier,
|
|
109
|
+
adapter_id: peakPricing.adapter_id,
|
|
110
|
+
},
|
|
96
111
|
features: decision.features ?? null,
|
|
97
112
|
delegate,
|
|
98
113
|
}),
|
|
@@ -521,6 +536,15 @@ export async function routeAndDelegate(
|
|
|
521
536
|
...(failoverNotice !== undefined ? { failoverNotice } : {}),
|
|
522
537
|
contextWindow: targetModel.contextWindow,
|
|
523
538
|
},
|
|
539
|
+
decision.request_id,
|
|
540
|
+
// SP-245 (#166): adaptive reasoning policy — effective thinking level
|
|
541
|
+
// from this turn's envelope + routing decision (recomputed per
|
|
542
|
+
// failover iteration so escalation targets re-evaluate).
|
|
543
|
+
{
|
|
544
|
+
turnType: request.turn_type,
|
|
545
|
+
decision,
|
|
546
|
+
...(targetProfile !== undefined ? { profile: targetProfile } : {}),
|
|
547
|
+
},
|
|
524
548
|
);
|
|
525
549
|
|
|
526
550
|
if (!isPipedResult(result)) {
|
|
@@ -761,6 +785,7 @@ export async function routeAndDelegate(
|
|
|
761
785
|
...(failoverNotice !== undefined ? { failoverNotice } : {}),
|
|
762
786
|
contextWindow: fallbackModel.contextWindow,
|
|
763
787
|
},
|
|
788
|
+
decision.request_id,
|
|
764
789
|
);
|
|
765
790
|
if (isPipedResult(fallbackResult)) {
|
|
766
791
|
commitPipedTerminal(fallbackResult);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
|
|
3
|
+
import { evictInMemorySessionState } from '../../../src/api/session-eviction.js';
|
|
3
4
|
import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
|
|
4
5
|
import {
|
|
5
6
|
bindSharedModelRegistry,
|
|
@@ -15,6 +16,15 @@ export const FLEET_MODE_ENTRY_TYPE = 'smart-router-fleet-mode' as const;
|
|
|
15
16
|
const SMART_ROUTER_PROVIDER = 'smart-router' as const;
|
|
16
17
|
const SMART_ROUTER_AUTO_ID = 'auto' as const;
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Orphan-session TTL fallback (SP-248, #145). `session_shutdown` is the primary
|
|
21
|
+
* teardown signal; if pi ever shuts a session down without delivering it
|
|
22
|
+
* (extension reload races, missed events), the session's in-memory routing
|
|
23
|
+
* state is evicted once it has been idle for ORPHAN_SESSION_TTL_MS. The sweep
|
|
24
|
+
* runs on `session_start` and fails open — eviction must never break startup.
|
|
25
|
+
*/
|
|
26
|
+
export const ORPHAN_SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
27
|
+
|
|
18
28
|
export function isSmartRouterActive(model: { provider: string; id: string }): boolean {
|
|
19
29
|
return model.provider === SMART_ROUTER_PROVIDER && model.id === SMART_ROUTER_AUTO_ID;
|
|
20
30
|
}
|
|
@@ -97,6 +107,39 @@ export function setupSessionHooks(
|
|
|
97
107
|
): void {
|
|
98
108
|
let activeModel: { provider: string; id: string } | undefined;
|
|
99
109
|
|
|
110
|
+
// Last-seen stamp per session for the orphan-TTL fallback (SP-248).
|
|
111
|
+
const lastSeenBySession = new Map<string, number>();
|
|
112
|
+
|
|
113
|
+
/** Evict all in-memory routing state for one session via the SP-247 helper. */
|
|
114
|
+
function evictSessionState(sessionId: string | undefined): void {
|
|
115
|
+
// Fail open: a missing/empty session id must never break teardown.
|
|
116
|
+
if (sessionId === undefined || sessionId === '') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
evictInMemorySessionState(sessionId, {
|
|
120
|
+
executionLedger: runtime.executionLedger,
|
|
121
|
+
lifecycleHookState: runtime.lifecycleHookState,
|
|
122
|
+
sessionRouting: runtime.sessionRouting,
|
|
123
|
+
});
|
|
124
|
+
lastSeenBySession.delete(sessionId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Drop sessions idle longer than ORPHAN_SESSION_TTL_MS (never throws). */
|
|
128
|
+
function sweepOrphanedSessions(now: number): void {
|
|
129
|
+
try {
|
|
130
|
+
for (const [sessionId, lastSeen] of lastSeenBySession) {
|
|
131
|
+
if (now - lastSeen > ORPHAN_SESSION_TTL_MS) {
|
|
132
|
+
evictSessionState(sessionId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.warn(
|
|
137
|
+
'[smart-router] orphan session sweep failed (fail open)',
|
|
138
|
+
error instanceof Error ? error.message : String(error),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
pi.on('session_start', async (_event, ctx) => {
|
|
101
144
|
activeModel = ctx.model;
|
|
102
145
|
bindSharedModelRegistry(runtime, ctx.modelRegistry);
|
|
@@ -128,6 +171,10 @@ export function setupSessionHooks(
|
|
|
128
171
|
datasetNotify.fn = runtime.notifyDatasetEnabled;
|
|
129
172
|
|
|
130
173
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
lastSeenBySession.set(sessionId, now);
|
|
176
|
+
sweepOrphanedSessions(now);
|
|
177
|
+
|
|
131
178
|
await sessionPinner.restoreSessionPin(sessionId);
|
|
132
179
|
|
|
133
180
|
if (ctx.model !== undefined && isSmartRouterActive(ctx.model)) {
|
|
@@ -158,5 +205,17 @@ export function setupSessionHooks(
|
|
|
158
205
|
delete runtime.streamDeps.ensureFleetFresh;
|
|
159
206
|
datasetNotify.fn = undefined;
|
|
160
207
|
ctx.ui.setStatus('smart-router-lmu', undefined);
|
|
208
|
+
|
|
209
|
+
// SP-248 (#145): drop per-session routing state so long-running pi
|
|
210
|
+
// processes do not retain ledger/lifecycle/routing maps forever.
|
|
211
|
+
// Fail open — teardown cleanup must never crash the host agent.
|
|
212
|
+
try {
|
|
213
|
+
evictSessionState(ctx.sessionManager.getSessionId());
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.warn(
|
|
216
|
+
'[smart-router] session teardown eviction failed (fail open)',
|
|
217
|
+
error instanceof Error ? error.message : String(error),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
161
220
|
});
|
|
162
221
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '@earendil-works/pi-ai/compat';
|
|
9
9
|
|
|
10
10
|
import type { RoutingDecision, RoutingFeatureSidecar } from '../../../src/domain/types/index.js';
|
|
11
|
+
import { resolvePeakPricingAdjustment } from '../../../src/domain/pricing/peak-pricing.js';
|
|
11
12
|
import { createErrorMessage } from './delegation-runtime.js';
|
|
12
13
|
import { routeAndDelegate } from './route-and-delegate.js';
|
|
13
14
|
import type { StreamDelegationDeps } from './types.js';
|
|
@@ -24,6 +25,14 @@ export function logRoutingDecision(
|
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
// SP-244 / #165: surface peak vs off-peak pricing rationale for the selected
|
|
29
|
+
// model (Z.ai GLM Coding Plan / DeepSeek API windows; multiplier 1 + window
|
|
30
|
+
// 'none' for non-target providers — fail open).
|
|
31
|
+
const peakPricing = resolvePeakPricingAdjustment({
|
|
32
|
+
id: delegate?.modelId ?? decision.selected_model_id,
|
|
33
|
+
...(delegate?.provider !== undefined ? { provider: delegate.provider } : {}),
|
|
34
|
+
});
|
|
35
|
+
|
|
27
36
|
console.warn(
|
|
28
37
|
'[smart-router] routing decision',
|
|
29
38
|
JSON.stringify({
|
|
@@ -33,6 +42,12 @@ export function logRoutingDecision(
|
|
|
33
42
|
stage: decision.stage,
|
|
34
43
|
reason_code: decision.reason_code,
|
|
35
44
|
routing_latency_ms: decision.routing_latency_ms,
|
|
45
|
+
pricing_window: peakPricing.window,
|
|
46
|
+
peak_pricing: {
|
|
47
|
+
window: peakPricing.window,
|
|
48
|
+
cost_multiplier: peakPricing.cost_multiplier,
|
|
49
|
+
adapter_id: peakPricing.adapter_id,
|
|
50
|
+
},
|
|
36
51
|
features: decision.features ?? null,
|
|
37
52
|
delegate,
|
|
38
53
|
}),
|
|
@@ -11,10 +11,13 @@ import type { HydraMatcher } from '../../../src/domain/matching/hydra-matcher.js
|
|
|
11
11
|
import { ExecutionLedger } from '../../../src/domain/delegation/execution-ledger.js';
|
|
12
12
|
import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
|
|
13
13
|
import type {
|
|
14
|
+
AdaptiveReasoningConfig,
|
|
14
15
|
ModelProfile,
|
|
15
16
|
PlanningDelegateConfig,
|
|
16
17
|
PriceCatalog,
|
|
17
18
|
RoutingDecision,
|
|
19
|
+
RoutingReasoningTelemetry,
|
|
20
|
+
RoutingUsageActuals,
|
|
18
21
|
} from '../../../src/domain/types/index.js';
|
|
19
22
|
import type { PlanningDelegateSpawnFn } from './planning-delegate.js';
|
|
20
23
|
import type { StorePort } from '../../../src/domain/types/store-port.js';
|
|
@@ -68,6 +71,8 @@ export interface StreamDelegationDeps {
|
|
|
68
71
|
spawnPlanningDelegate?: PlanningDelegateSpawnFn;
|
|
69
72
|
/** Planning delegate knobs incl. global + per-call timeout bounds (SP-213, #120). */
|
|
70
73
|
readonly planningDelegateConfig?: PlanningDelegateConfig;
|
|
74
|
+
/** Adaptive reasoning knobs: enable/disable + floor/ceiling (SP-246, #166). */
|
|
75
|
+
readonly adaptiveReasoningConfig?: AdaptiveReasoningConfig;
|
|
71
76
|
readonly lifecycleHookState?: LifecycleHookState;
|
|
72
77
|
readonly datasetRecorder?: DatasetRecorder;
|
|
73
78
|
readonly outcomeRecorder?: OutcomeRecorder;
|
|
@@ -82,6 +87,18 @@ export interface StreamDelegationDeps {
|
|
|
82
87
|
readonly contextWindow?: number;
|
|
83
88
|
readonly maxTokens?: number;
|
|
84
89
|
}) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Fired after a delegated stream ends with host-reported usage actuals
|
|
92
|
+
* (SP-241, #164). Also fires on failed-with-usage terminals. Implementations
|
|
93
|
+
* must not throw — actuals capture never fails the route.
|
|
94
|
+
*/
|
|
95
|
+
onDelegationUsage?: (requestId: string, actuals: RoutingUsageActuals) => void;
|
|
96
|
+
/**
|
|
97
|
+
* Fired after the adaptive reasoning policy resolves the effective thinking
|
|
98
|
+
* level for a delegated stream (SP-246, #166). Implementations must not
|
|
99
|
+
* throw — reasoning telemetry must never fail the route.
|
|
100
|
+
*/
|
|
101
|
+
onDelegationReasoning?: (requestId: string, fields: RoutingReasoningTelemetry) => void;
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
export interface SmartRouterRuntime {
|