pi-smart-router 0.19.4 → 0.20.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/stream-delegation.ts +15 -0
- package/.pi/extensions/smart-router/types.ts +17 -0
- package/README.md +82 -1
- package/config/operator-config.json.example +10 -0
- 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/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 +64 -0
- package/dist/domain/types/schemas.d.ts.map +1 -1
- package/dist/domain/types/schemas.js +84 -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/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/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/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 +101 -0
- package/src/domain/types/store-port.ts +18 -1
- 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);
|
|
@@ -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 {
|
package/README.md
CHANGED
|
@@ -218,7 +218,7 @@ Cursor models bill against your **Cursor Pro subscription quota**, not per-token
|
|
|
218
218
|
| `/smart-router` | Same as `status` (default when no subcommand is given) |
|
|
219
219
|
| `/smart-router status` | Show fleet mode, fleet size, pricing freshness/staleness, and the last routing decision (stage, tier, selected model, latency) |
|
|
220
220
|
| `/smart-router history` | Show recent routing telemetry from SQLite (default limit; optional numeric limit, e.g. `/smart-router history 20`). Displays the concrete delegated model id (never bare virtual `auto`) |
|
|
221
|
-
| `/smart-router stats` | Privacy-safe session/window aggregates from routing telemetry: count, mean cost/latency, planning_delegate vs direct share, local vs cloud when distinguishable, and role cost breakdown
|
|
221
|
+
| `/smart-router stats` | Privacy-safe session/window aggregates from routing telemetry: count, mean cost/latency with `cost_basis` labeling (actual vs estimated, SP-241), planning_delegate vs direct share, local vs cloud when distinguishable, and role cost breakdown. Optional vs-always-frontier savings when frontier fleet prices exist (omitted otherwise). The JSON snapshot (`buildStatsSnapshot`, automation surface) additionally carries warm rolling `cost_calibration` actual/estimate buckets (SP-242). Optional numeric limit, e.g. `/smart-router stats 50` |
|
|
222
222
|
| `/smart-router mode scoped` | Route only among pi's **enabled model patterns** (default) |
|
|
223
223
|
| `/smart-router mode all` | Route among **all authenticated models** in the registry |
|
|
224
224
|
| `/smart-router pricing refresh` | Manually fetch LiteLLM pricing from `LITELLM_PRICING_URL`, persist to SQLite, and rebuild the fleet with updated rates |
|
|
@@ -441,6 +441,9 @@ Cluster IDs are stable reason-code prefixes (`cluster_low_stakes_general`, `clus
|
|
|
441
441
|
| `SMART_ROUTER_PLANNING_DELEGATE_EXCLUDE_EXECUTION_HISTORY` | `true` | Exclude tool execution history from delegate payload |
|
|
442
442
|
| `SMART_ROUTER_PLANNING_DELEGATE_GLOBAL_TIMEOUT_MS` | `120000` | Global cap (ms) on the whole planning-delegate stage per planning turn — bounds fan-out wall-clock so a stalled worker cannot hang TTFT ([#120](https://github.com/beettlle/pi-smart-router/issues/120)) |
|
|
443
443
|
| `SMART_ROUTER_PLANNING_DELEGATE_SUB_CALL_TIMEOUT_MS` | `30000` | Per-call cap (ms) on each delegate sub-call worker; on expiry the worker is cancelled/abandoned and routing falls back to direct frontier with `planning_delegate_timeout` ([#120](https://github.com/beettlle/pi-smart-router/issues/120)) |
|
|
444
|
+
| `SMART_ROUTER_ADAPTIVE_REASONING_ENABLED` | `true` | Master switch for the adaptive thinking-level policy ([#166](https://github.com/beettlle/pi-smart-router/issues/166)); `false` passes the session thinking level through unchanged |
|
|
445
|
+
| `SMART_ROUTER_ADAPTIVE_REASONING_MIN_LEVEL` | (unset) | Floor on policy-derived thinking levels (`minimal\|low\|medium\|high\|xhigh\|max`) — see [Adaptive reasoning](#adaptive-reasoning-thinking-level-166) |
|
|
446
|
+
| `SMART_ROUTER_ADAPTIVE_REASONING_MAX_LEVEL` | (unset) | Ceiling on policy-derived thinking levels (incl. turn-class upgrades) — see [Adaptive reasoning](#adaptive-reasoning-thinking-level-166) |
|
|
444
447
|
| `SMART_ROUTER_PREFIX_CACHE_WEIGHT` | `0.20` | SAAR weight on warm prefix value in cache breakeven math (0–1; [#73](https://github.com/beettlle/pi-smart-router/issues/73)) |
|
|
445
448
|
| `SMART_ROUTER_IDLE_TIMEOUT_SECONDS` | `300` | SAAR idle seconds before pin reopens for full re-route |
|
|
446
449
|
| `SMART_ROUTER_SWITCH_THRESHOLD` | `0.5` | SAAR switch score gate (0–1) for tier upgrades during hard-lock |
|
|
@@ -459,6 +462,7 @@ When `SMART_ROUTER_LOG_ROUTING=1`, prefer the canonical payload from `buildRouti
|
|
|
459
462
|
| `tier_hint` | Yes (top-level + `cluster_summary`) | Null when no tier hint |
|
|
460
463
|
| `local_eligible_reason` | Yes (top-level + `features`) | Null when local_zero did not evaluate eligibility |
|
|
461
464
|
| `cluster_id` | Yes (top-level + `cluster_summary`) | Null when no cluster match |
|
|
465
|
+
| `pricing_window` | Yes (top-level + `peak_pricing_summary`) | Peak vs off-peak rationale for the selected model (SP-244 / #165); the extension stderr logger carries `pricing_window` + `peak_pricing` |
|
|
462
466
|
|
|
463
467
|
**Gap:** the pi extension’s live stderr path (`logRoutingDecision` in `.pi/extensions/smart-router`) still emits a slim JSON object (`selected_model_id`, `stage`, `reason_code`, `features`, `delegate`) and does **not** yet call `buildRoutingDecisionLogPayload`. SQLite `/smart-router history` and the payload builder carry the full checklist; wire the extension logger in a follow-up if dogfood needs identical stderr shape.
|
|
464
468
|
|
|
@@ -515,6 +519,41 @@ When a **planning** turn would route primary inference to frontier while a warm
|
|
|
515
519
|
|
|
516
520
|
See [routing-roadmap.md](docs/routing-roadmap.md) §2 P0 and GitHub [#71](https://github.com/beettlle/pi-smart-router/issues/71) for acceptance criteria.
|
|
517
521
|
|
|
522
|
+
### Adaptive reasoning (thinking level) (#166)
|
|
523
|
+
|
|
524
|
+
Adaptive reasoning tunes the **thinking intensity of the model already selected** — it never changes which model runs. Per turn class:
|
|
525
|
+
|
|
526
|
+
| Turn class | Policy level |
|
|
527
|
+
|-----------|--------------|
|
|
528
|
+
| `tool_result` | `minimal` |
|
|
529
|
+
| `main_loop` | `low` |
|
|
530
|
+
| planning / `planning_delegate` | `medium` |
|
|
531
|
+
| frontier escalation / `loop_escalation` | `high` |
|
|
532
|
+
|
|
533
|
+
pi passes the session thinking level on every call; the router treats pi's ambient default (`medium`) as adjustable by policy, and any **other** explicit level as an operator `/thinking` floor that is **never lowered** (a turn-class upgrade may still raise it). Chatty profiles (high `verbosity_factor`) additionally get a one-line conciseness nudge at `minimal`/`low`.
|
|
534
|
+
|
|
535
|
+
**Three knobs that sound alike, do different things:**
|
|
536
|
+
|
|
537
|
+
| Knob | Acts on | What it changes |
|
|
538
|
+
|------|---------|-----------------|
|
|
539
|
+
| **Adaptive reasoning** (`adaptive_reasoning.*`) | The delegated call's `reasoning` option | Thinking *intensity* of the already-selected model — cost of thinking, not model choice |
|
|
540
|
+
| `frugality.lambda_verbosity` | Multi-objective **selection** scoring | Which model gets picked — penalizes verbose models while ranking candidates; never touches the delegated call's reasoning option |
|
|
541
|
+
| `/thinking` (pi session command) | The caller-provided reasoning level | Explicit operator override — an explicit level is never lowered by policy or bounds |
|
|
542
|
+
|
|
543
|
+
**Operator knobs** (config `adaptive_reasoning` / env):
|
|
544
|
+
|
|
545
|
+
| Key | Env var | Default | Effect |
|
|
546
|
+
|-----|---------|---------|--------|
|
|
547
|
+
| `enabled` | `SMART_ROUTER_ADAPTIVE_REASONING_ENABLED` | `true` | When `false`, the policy is skipped — delegated calls pass the session thinking level through unchanged (`reasoning_reason_code: adaptive_reasoning_disabled`) |
|
|
548
|
+
| `min_level` | `SMART_ROUTER_ADAPTIVE_REASONING_MIN_LEVEL` | (none) | Floor: policy-derived levels are raised to at least this level. Discrete level (`minimal\|low\|medium\|high\|xhigh\|max`) — deliberately **not** a free-form verbosity percent |
|
|
549
|
+
| `max_level` | `SMART_ROUTER_ADAPTIVE_REASONING_MAX_LEVEL` | (none) | Ceiling: policy-derived levels (incl. turn-class upgrades) are capped at this level. When `min_level` exceeds `max_level` (e.g. via env), the ceiling wins (cost-safe) |
|
|
550
|
+
|
|
551
|
+
Floor/ceiling bind only what the policy itself derives. An explicit operator `/thinking` choice is never **lowered** by either bound (a floor can still *raise* one via the policy-upgrade path). Both bounds re-clamp down to the model's supported levels.
|
|
552
|
+
|
|
553
|
+
**Fail-open behavior:** models that do not support reasoning options (`reasoning: false`, or a `thinkingLevelMap` mapping every relevant level to `null`) pass caller options through unchanged — telemetry records `reasoning_reason_code: reasoning_unsupported` and the route never fails. Providers that ignore reasoning options degrade to a no-op.
|
|
554
|
+
|
|
555
|
+
**Telemetry:** each routed delegation records `reasoning_level_requested` (the session/caller level), `reasoning_level_applied` (the effective delegated level), and `reasoning_reason_code` (e.g. `turn_envelope_main_loop`, `operator_thinking_floor`, `operator_floor_applied`, `operator_ceiling_applied`, `reasoning_unsupported`) on the routing telemetry row — enriched post-delegation like usage actuals ([SP-241](https://github.com/beettlle/pi-smart-router/issues/164)). Inspect via `/smart-router history`.
|
|
556
|
+
|
|
518
557
|
### Degraded neural failover sandwich (#119)
|
|
519
558
|
|
|
520
559
|
When the encoder/neural stage (HyDRA) **fails, is misconfigured, or exceeds its latency budget without a selection**, routing fails open through a cheap chain instead of crashing the host agent ([#119](https://github.com/beettlle/pi-smart-router/issues/119)):
|
|
@@ -587,6 +626,48 @@ When `quotaWindowPosition` is omitted, λ stays at 1 and quota premiums are zero
|
|
|
587
626
|
|
|
588
627
|
See [routing-roadmap.md](docs/routing-roadmap.md) §2 P2 and GitHub [#78](https://github.com/beettlle/pi-smart-router/issues/78).
|
|
589
628
|
|
|
629
|
+
### Usage actuals (post-turn capture)
|
|
630
|
+
|
|
631
|
+
When pi reports an assistant message `usage` object after a delegated turn, the smart-router persists it onto that turn's routing telemetry row ([#164](https://github.com/beettlle/pi-smart-router/issues/164)): `actual_cost_usd`, `actual_input_tokens`, `actual_output_tokens`, and cache read/write token counts — while retaining `estimated_cost_usd`. Capture fails open: library embeds and non-pi hosts that report no usage simply leave the actual fields null, and a telemetry write error never fails the route. Subscription/OAuth models report `cost.total === 0`: token actuals are still recorded, but no USD is invented — `/smart-router stats` labels its totals `cost_basis: 'actual' | 'estimated' | 'mixed'` accordingly.
|
|
632
|
+
|
|
633
|
+
### Rolling cost calibration (v0.20.0 usage actuals)
|
|
634
|
+
|
|
635
|
+
**Rolling cost calibration** soft-biases future cost estimates with the ratio your models *actually* bill versus what the router estimated ([#164](https://github.com/beettlle/pi-smart-router/issues/164)). It is built from the privacy-safe post-turn [usage actuals](#usage-actuals-post-turn-capture) — no new state to configure, and prompt/message bodies are never touched.
|
|
636
|
+
|
|
637
|
+
**How it works**
|
|
638
|
+
|
|
639
|
+
1. Every routed turn records an `estimated_cost_usd` (input tokens × catalog rate) and, when pi reports it, an `actual_cost_usd` on the same telemetry row.
|
|
640
|
+
2. `buildCostCalibrationPrior` derives per-**model** and per-**tier** mean `actual / estimate` ratios over the rolling telemetry window. Per-pair outliers are clamped (±10×) before averaging; the aggregate is clamped to a soft band of **[0.5, 2.0]** so calibration can never hard-ban or hard-favor a model on cost alone.
|
|
641
|
+
3. When warm (≥ 3 usable pairs per bucket), the ratio multiplies the tier's base per-1M cost **before** the virtual-cost v2 λ/premium/KV chain — so quota decay and cache credits compound on the calibrated base. Model buckets win over tier buckets.
|
|
642
|
+
4. `estimateRoutingCost` accepts the same prior as an optional argument, soft-biasing per-request cost estimates identically.
|
|
643
|
+
|
|
644
|
+
**Cold start fails open.** No prior, an empty prior, or a bucket below the warmup threshold resolves ratio 1 — the catalog estimate is used unchanged. Subscription rows (host reports `cost.total === 0`) never contribute: stats and calibration never invent USD.
|
|
645
|
+
|
|
646
|
+
**Where it applies**
|
|
647
|
+
|
|
648
|
+
- **Expected-cost tier selection** — pass `costCalibration` into `selectTierByExpectedCost` / `computeExpectedCost` (library API, same pattern as `heatBias`). A warm ratio that doubles an economical tier's effective cost can flip the next selection to frontier; the price-delta and pin-economics hard gates still apply after the soft bias.
|
|
649
|
+
- **Pre-route estimates** — `estimateRoutingCost(model, request, catalog, calibration?)`; the router pipeline's uncalibrated call is unchanged.
|
|
650
|
+
- **`/smart-router stats`** — the aggregate's JSON snapshot (`aggregateSessionStats` / `buildStatsSnapshot`, the automation/MCP surface) carries a `cost_calibration` array (model buckets first, then tier buckets, each `{key, kind, ratio, samples}`); omitted entirely when cold so automation can treat absence as catalog-only. The human-readable stats text keeps rendering cost basis and role breakdown only.
|
|
651
|
+
|
|
652
|
+
**Observing the bias** — run with `SMART_ROUTER_LOG_ROUTING=1` and read the expected-cost gate line: calibrated winners carry a `[cost-calib ×N.NN from rolling actuals (SP-242)]` note on the rationale, and each tier's `calibrationRatio` appears in the expected-cost breakdown (`features.tier_selection.tier_costs[]`).
|
|
653
|
+
|
|
654
|
+
**Knobs** (`DEFAULT_COST_CALIBRATION_CONFIG`): `minSamples` 3 (warmup), `minRatio`/`maxRatio` 0.5/2.0 (soft band), `sampleMinRatio`/`sampleMaxRatio` 0.1/10 (per-pair outlier clamp). Not a vendor peak-clock schedule — see [#165](https://github.com/beettlle/pi-smart-router/issues/165) for time-of-day pricing.
|
|
655
|
+
|
|
656
|
+
### Peak/off-peak pricing adapters (v0.20.0, #165)
|
|
657
|
+
|
|
658
|
+
Two vendors publish documented **time-of-day rate cards**. The peak-pricing adapters (`src/domain/pricing/peak-pricing.js`, SP-243) soft-bias the resolved cost-per-1M by the current pricing window — they never hard-ban a model, and non-target providers (OpenAI / Anthropic / Gemini / local / unknown) always resolve `window: 'none'` with multiplier 1 (fail open).
|
|
659
|
+
|
|
660
|
+
| Vendor | Adapter | Peak window | Off-peak rate | Docs |
|
|
661
|
+
|--------|---------|-------------|---------------|------|
|
|
662
|
+
| Z.ai GLM Coding Plan | `zai` (matches `zai`/`glm`/`zhipu` providers and `glm-*` ids) | Mon–Fri 14:00–18:00 Asia/Singapore (UTC+8) | 0.5× the standard credit rate | [Z.ai GLM Coding Plan docs](https://docs.z.ai/devpack/overview) |
|
|
663
|
+
| DeepSeek pay-as-you-go API | `deepseek` (matches `deepseek` provider / ids) | Mon–Fri 01:00–04:00 and 06:00–10:00 UTC | ½ the peak rate on cache-hit input, cache-miss input, and output | [DeepSeek Models & Pricing](https://api-docs.deepseek.com/quick_start/pricing) |
|
|
664
|
+
|
|
665
|
+
**Z.ai plan profiles.** The default plan profile is `credits` — off-peak usage at 0.5× the standard credit rate, peak at 1×. **Legacy plans** (e.g. GLM-5.3 legacy at 3× peak / 1× off-peak, Flash at 1.2× / 0.4×) share the same window but use different multipliers; they are available **only** via an explicit operator override (`PeakPricingConfig.zai`: `plan_profile: 'legacy'` plus documented `peak_multiplier` / `off_peak_multiplier`). The adapter never scrapes the live account plan — legacy multipliers are documented override inputs, not detected state.
|
|
666
|
+
|
|
667
|
+
**Configuration.** Adapters are on by default; `PeakPricingConfig.enabled: false` returns to flat rates. `PeakPricingConfig.deepseek.off_peak_multiplier` overrides the documented 0.5 off-peak discount if DeepSeek changes its schedule.
|
|
668
|
+
|
|
669
|
+
**Observing the window.** Every routed turn records `pricing_window: 'peak' | 'off_peak' | 'none'` on its telemetry row (SP-243). With `SMART_ROUTER_LOG_ROUTING=1`, the routing-decision payload surfaces the rationale as top-level `pricing_window` plus `peak_pricing_summary` (`window`, `cost_multiplier`, `adapter_id`) on the canonical `buildRoutingDecisionLogPayload`, and `pricing_window` + `peak_pricing` on the extension stderr logger (SP-244).
|
|
670
|
+
|
|
590
671
|
### P(success) training export (baseline classifier)
|
|
591
672
|
|
|
592
673
|
When `SMART_ROUTER_DATASET=1`, the router records privacy-safe dataset rows and behavioral outcome labels. Export labeled training data from pi:
|
|
@@ -74,5 +74,15 @@
|
|
|
74
74
|
"max_tool_use_requirement": "Ceiling (0–1) on cheap predicted tool_use for local_zero dispatch. Effective limit is min(local model tool_use capability, this value). Default 0.25 keeps format/lint local while skipping agentic git/bash/edit/explore/delete/repo cues (SP-177, #98)."
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"adaptive_reasoning": {
|
|
78
|
+
"enabled": true,
|
|
79
|
+
"min_level": "low",
|
|
80
|
+
"max_level": "high",
|
|
81
|
+
"_documentation": {
|
|
82
|
+
"enabled": "When false, adaptive reasoning is skipped — delegated calls pass the caller (session) thinking level through unchanged. Default true (SP-246, #166).",
|
|
83
|
+
"min_level": "Optional floor on policy-derived thinking levels (minimal | low | medium | high | xhigh | max). Routine tool_result/main_loop turns policy-resolve to minimal/low; a floor raises that. Never lowers an explicit operator /thinking choice. Omit for no floor.",
|
|
84
|
+
"max_level": "Optional ceiling on policy-derived thinking levels, including turn-class upgrades. When min_level would exceed max_level (e.g. via env), the ceiling wins. Omit for no ceiling."
|
|
85
|
+
}
|
|
86
|
+
},
|
|
77
87
|
"pin_only_fallback": false
|
|
78
88
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Values sourced from specs/001-build-smart-router/data-model.md § Configuration (Operator).
|
|
4
4
|
*/
|
|
5
5
|
import { type OperatorConfig } from '../domain/types/schemas.js';
|
|
6
|
-
export { DEFAULT_LOCAL_ZERO_CONFIG, DEFAULT_PLANNING_DELEGATE_CONFIG, DEFAULT_SAAR_CONFIG, DEFAULT_SPECULATIVE_PREWARM_CONFIG, DEFAULT_WORKLOAD_HEAT_CONFIG, resolvePlanningDelegateConfigFromEnv, resolveSaarConfigFromEnv, } from '../domain/types/schemas.js';
|
|
7
|
-
/** Merge operator env overrides onto defaults (SAAR and planning delegate sections). */
|
|
6
|
+
export { DEFAULT_ADAPTIVE_REASONING_CONFIG, DEFAULT_LOCAL_ZERO_CONFIG, DEFAULT_PLANNING_DELEGATE_CONFIG, DEFAULT_SAAR_CONFIG, DEFAULT_SPECULATIVE_PREWARM_CONFIG, DEFAULT_WORKLOAD_HEAT_CONFIG, resolveAdaptiveReasoningConfigFromEnv, resolvePlanningDelegateConfigFromEnv, resolveSaarConfigFromEnv, } from '../domain/types/schemas.js';
|
|
7
|
+
/** Merge operator env overrides onto defaults (adaptive reasoning, SAAR and planning delegate sections). */
|
|
8
8
|
export declare function resolveOperatorConfigFromEnv(base?: OperatorConfig): OperatorConfig;
|
|
9
9
|
export declare const DEFAULT_OPERATOR_CONFIG: Readonly<OperatorConfig>;
|
|
10
10
|
//# sourceMappingURL=defaults.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAWL,KAAK,cAAc,EACpB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACL,iCAAiC,EACjC,yBAAyB,EACzB,gCAAgC,EAChC,mBAAmB,EACnB,kCAAkC,EAClC,4BAA4B,EAC5B,qCAAqC,EACrC,oCAAoC,EACpC,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AAEpC,4GAA4G;AAC5G,wBAAgB,4BAA4B,CAC1C,IAAI,GAAE,cAAwC,GAC7C,cAAc,CAOhB;AAED,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,cAAc,CAuCnD,CAAC"}
|