pi-smart-router 0.16.2 → 0.17.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/route-and-delegate.ts +98 -6
- package/.pi/extensions/smart-router/routing-context.ts +77 -16
- package/README.md +27 -5
- package/config/benchmark-profiles.json +2 -2
- package/config/operator-config.json.example +6 -0
- package/dist/domain/pipeline/router-pipeline.d.ts +26 -0
- package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -1
- package/dist/domain/pipeline/router-pipeline.js +39 -0
- package/dist/domain/pipeline/router-pipeline.js.map +1 -1
- package/dist/domain/routing/p-success-classifier.d.ts +2 -2
- package/dist/domain/types/entities.d.ts +17 -1
- package/dist/domain/types/entities.d.ts.map +1 -1
- package/dist/domain/types/schemas.d.ts +505 -8
- package/dist/domain/types/schemas.d.ts.map +1 -1
- package/dist/domain/types/schemas.js +188 -9
- package/dist/domain/types/schemas.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/telemetry/routing-telemetry.d.ts +5 -5
- package/package.json +1 -1
- package/src/domain/pipeline/router-pipeline.ts +49 -0
- package/src/domain/types/entities.ts +24 -1
- package/src/domain/types/schemas.ts +231 -37
- package/src/index.ts +11 -0
|
@@ -52,6 +52,11 @@ function isPipedResult(
|
|
|
52
52
|
return 'heldTerminal' in result;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** Fail-open reason codes (SP-226) — emitted in telemetry and SMART_ROUTER_LOG_ROUTING=1. */
|
|
56
|
+
export const NO_REGISTRY_MODEL = 'no_registry_model';
|
|
57
|
+
export const FAILOVER_EXHAUSTED = 'failover_exhausted';
|
|
58
|
+
export const DELEGATION_ABORTED = 'delegation_aborted';
|
|
59
|
+
|
|
55
60
|
function isRoutingLogEnabled(): boolean {
|
|
56
61
|
return process.env.SMART_ROUTER_LOG_ROUTING === '1';
|
|
57
62
|
}
|
|
@@ -152,6 +157,68 @@ function isZeroOutputLengthStop(message: AssistantMessage): boolean {
|
|
|
152
157
|
return message.stopReason === 'length' && message.usage.output === 0;
|
|
153
158
|
}
|
|
154
159
|
|
|
160
|
+
/** Minimal model identity for degraded terminal messages when no Model resolved. */
|
|
161
|
+
interface DegradedModelRef {
|
|
162
|
+
readonly api: Api;
|
|
163
|
+
readonly provider: string;
|
|
164
|
+
readonly id: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createDegradedErrorMessage(
|
|
168
|
+
model: DegradedModelRef,
|
|
169
|
+
reasonCode: string,
|
|
170
|
+
detail: string,
|
|
171
|
+
): AssistantMessage {
|
|
172
|
+
return {
|
|
173
|
+
role: 'assistant',
|
|
174
|
+
content: [],
|
|
175
|
+
api: model.api,
|
|
176
|
+
provider: model.provider,
|
|
177
|
+
model: model.id,
|
|
178
|
+
usage: {
|
|
179
|
+
input: 0,
|
|
180
|
+
output: 0,
|
|
181
|
+
cacheRead: 0,
|
|
182
|
+
cacheWrite: 0,
|
|
183
|
+
totalTokens: 0,
|
|
184
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
185
|
+
},
|
|
186
|
+
stopReason: 'error',
|
|
187
|
+
errorMessage: `Smart router degraded response (${reasonCode}): ${detail}`,
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Fail-open terminal (SP-226): never throw to the host on exhaustion paths.
|
|
194
|
+
* Emit a structured warning with the reason code and end the outer stream with
|
|
195
|
+
* a degraded error message so the pi host receives an actionable response.
|
|
196
|
+
*/
|
|
197
|
+
function emitDegradedFailure(
|
|
198
|
+
outer: AssistantMessageEventStream,
|
|
199
|
+
model: DegradedModelRef | undefined,
|
|
200
|
+
selectedModelId: string,
|
|
201
|
+
reasonCode: string,
|
|
202
|
+
detail: string,
|
|
203
|
+
): void {
|
|
204
|
+
console.warn(
|
|
205
|
+
'[smart-router] fail-open degraded response',
|
|
206
|
+
JSON.stringify({
|
|
207
|
+
reason_code: reasonCode,
|
|
208
|
+
selected_model_id: selectedModelId,
|
|
209
|
+
detail,
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
const ref: DegradedModelRef = model ?? {
|
|
213
|
+
api: 'unknown',
|
|
214
|
+
provider: 'unknown',
|
|
215
|
+
id: selectedModelId,
|
|
216
|
+
};
|
|
217
|
+
const errorMessage = createDegradedErrorMessage(ref, reasonCode, detail);
|
|
218
|
+
outer.push({ type: 'error', reason: 'error', error: errorMessage });
|
|
219
|
+
outer.end(errorMessage);
|
|
220
|
+
}
|
|
221
|
+
|
|
155
222
|
function buildOverflowRoutingDecision(
|
|
156
223
|
base: RoutingDecision,
|
|
157
224
|
fallback: ReturnType<typeof resolveContextOverflowFallback>,
|
|
@@ -357,9 +424,18 @@ export async function routeAndDelegate(
|
|
|
357
424
|
if (decision.selected_model_id === 'unknown' && guardResult) {
|
|
358
425
|
assertRoutableFleetAfterGeminiToolHistoryGuard(guardResult);
|
|
359
426
|
}
|
|
360
|
-
|
|
427
|
+
// SP-226 fail-open: no registry model resolved — degrade instead of throwing.
|
|
428
|
+
deps.router.dispatch.recordOutcome(decision.selected_model_id, {
|
|
429
|
+
code: 'NO_REGISTRY_MODEL',
|
|
430
|
+
});
|
|
431
|
+
emitDegradedFailure(
|
|
432
|
+
outer,
|
|
433
|
+
undefined,
|
|
434
|
+
decision.selected_model_id,
|
|
435
|
+
NO_REGISTRY_MODEL,
|
|
361
436
|
`No registry model available for routing decision ${decision.selected_model_id}`,
|
|
362
437
|
);
|
|
438
|
+
return;
|
|
363
439
|
}
|
|
364
440
|
|
|
365
441
|
logRoutingDecision(decision, {
|
|
@@ -546,6 +622,16 @@ export async function routeAndDelegate(
|
|
|
546
622
|
return;
|
|
547
623
|
} catch (error) {
|
|
548
624
|
if (isAbortError(error, options)) {
|
|
625
|
+
// SP-226: telemetry for phase-boundary aborts (previously silent).
|
|
626
|
+
if (isRoutingLogEnabled()) {
|
|
627
|
+
console.warn(
|
|
628
|
+
'[smart-router] delegation aborted',
|
|
629
|
+
JSON.stringify({
|
|
630
|
+
reason_code: DELEGATION_ABORTED,
|
|
631
|
+
model_id: targetModel.id,
|
|
632
|
+
}),
|
|
633
|
+
);
|
|
634
|
+
}
|
|
549
635
|
const abortMessage = createErrorMessage(targetModel, options, error);
|
|
550
636
|
outer.push({ type: 'error', reason: 'aborted', error: abortMessage });
|
|
551
637
|
outer.end(abortMessage);
|
|
@@ -565,7 +651,7 @@ export async function routeAndDelegate(
|
|
|
565
651
|
);
|
|
566
652
|
const alternateModel = failover ? resolveTargetModel(deps, failover) : undefined;
|
|
567
653
|
|
|
568
|
-
if (alternateModel && alternateModel.id !== targetModel.id) {
|
|
654
|
+
if (failover && alternateModel && alternateModel.id !== targetModel.id) {
|
|
569
655
|
console.warn(
|
|
570
656
|
'[smart-router] stream delegation failed, failing over',
|
|
571
657
|
error instanceof Error ? error.message : String(error),
|
|
@@ -575,9 +661,6 @@ export async function routeAndDelegate(
|
|
|
575
661
|
alternateModelId: alternateModel.id,
|
|
576
662
|
errorObj: { message: error instanceof Error ? error.message : String(error) },
|
|
577
663
|
};
|
|
578
|
-
if (!failover) {
|
|
579
|
-
throw error;
|
|
580
|
-
}
|
|
581
664
|
decision = failover;
|
|
582
665
|
targetModel = alternateModel;
|
|
583
666
|
continue;
|
|
@@ -585,7 +668,16 @@ export async function routeAndDelegate(
|
|
|
585
668
|
|
|
586
669
|
const fallbackModel = resolveFallbackModel(deps, effectiveFleet);
|
|
587
670
|
if (!fallbackModel || fallbackModel.id === targetModel.id) {
|
|
588
|
-
|
|
671
|
+
// SP-226 fail-open: fleet/failover exhausted and no distinct safe
|
|
672
|
+
// default — degrade instead of throwing to the host.
|
|
673
|
+
emitDegradedFailure(
|
|
674
|
+
outer,
|
|
675
|
+
targetModel,
|
|
676
|
+
decision.selected_model_id,
|
|
677
|
+
FAILOVER_EXHAUSTED,
|
|
678
|
+
error instanceof Error ? error.message : String(error),
|
|
679
|
+
);
|
|
680
|
+
return;
|
|
589
681
|
}
|
|
590
682
|
|
|
591
683
|
console.warn(
|
|
@@ -76,6 +76,35 @@ function messageContentToString(content: string | readonly (TextContent | { type
|
|
|
76
76
|
.join('\n');
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* SP-225 / #137: read an HTTP-ish status a host may attach to a tool result
|
|
81
|
+
* (either directly on the message or inside `details`). Returns undefined
|
|
82
|
+
* when no finite numeric status is present.
|
|
83
|
+
*/
|
|
84
|
+
function readOptionalStatus(source: unknown): number | undefined {
|
|
85
|
+
if (source === null || typeof source !== 'object') {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const record = source as Record<string, unknown>;
|
|
89
|
+
const direct = record.status;
|
|
90
|
+
if (typeof direct === 'number' && Number.isFinite(direct) && direct >= 0) {
|
|
91
|
+
return Math.floor(direct);
|
|
92
|
+
}
|
|
93
|
+
return readOptionalStatus(record.details);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface MapContextMessagesOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Opt-in: include assistant `thinking` blocks in routing `content`.
|
|
99
|
+
* Default false — thinking is model-internal reasoning, not a routing
|
|
100
|
+
* signal, and leaking it inflates token estimates (#137).
|
|
101
|
+
*/
|
|
102
|
+
includeThinking?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Operator env gate restoring the pre-SP-225 thinking-in-content behavior. */
|
|
106
|
+
const INCLUDE_THINKING_ENV = 'SMART_ROUTER_INCLUDE_THINKING';
|
|
107
|
+
|
|
79
108
|
export function extractPromptText(messages: readonly Message[]): string {
|
|
80
109
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
81
110
|
const message = messages[i];
|
|
@@ -117,7 +146,11 @@ export function deriveTurnType(messages: readonly Message[]): TurnType {
|
|
|
117
146
|
return 'main_loop';
|
|
118
147
|
}
|
|
119
148
|
|
|
120
|
-
export function mapContextMessages(
|
|
149
|
+
export function mapContextMessages(
|
|
150
|
+
messages: readonly Message[],
|
|
151
|
+
options?: MapContextMessagesOptions,
|
|
152
|
+
): RoutingMessage[] {
|
|
153
|
+
const includeThinking = options?.includeThinking === true;
|
|
121
154
|
return messages.map((message) => {
|
|
122
155
|
if (message.role === 'user') {
|
|
123
156
|
return {
|
|
@@ -127,27 +160,53 @@ export function mapContextMessages(messages: readonly Message[]): RoutingMessage
|
|
|
127
160
|
}
|
|
128
161
|
|
|
129
162
|
if (message.role === 'assistant') {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
163
|
+
const contentParts: string[] = [];
|
|
164
|
+
const toolBlocks: Record<string, unknown>[] = [];
|
|
165
|
+
for (const block of message.content) {
|
|
166
|
+
if (block.type === 'text') {
|
|
167
|
+
contentParts.push(block.text);
|
|
168
|
+
} else if (block.type === 'thinking') {
|
|
169
|
+
if (includeThinking) {
|
|
170
|
+
contentParts.push(block.thinking);
|
|
134
171
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
172
|
+
} else if (block.type === 'toolCall') {
|
|
173
|
+
toolBlocks.push({
|
|
174
|
+
type: 'tool_call',
|
|
175
|
+
tool_call_id: block.id,
|
|
176
|
+
tool_name: block.name,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
142
180
|
|
|
143
|
-
|
|
181
|
+
const mapped: RoutingMessage = {
|
|
182
|
+
role: message.role,
|
|
183
|
+
content: contentParts.filter(Boolean).join('\n'),
|
|
184
|
+
};
|
|
185
|
+
if (toolBlocks.length > 0) {
|
|
186
|
+
return { ...mapped, tool_blocks: toolBlocks };
|
|
187
|
+
}
|
|
188
|
+
return mapped;
|
|
144
189
|
}
|
|
145
190
|
|
|
191
|
+
const status = readOptionalStatus(message);
|
|
192
|
+
// When a host attaches an HTTP-ish status, let the domain status>=400 rule
|
|
193
|
+
// arbitrate (#137): a bare isError=false would otherwise mask the
|
|
194
|
+
// structured signal. Without a status, preserve the host isError verbatim.
|
|
195
|
+
const isError = message.isError === true || status === undefined
|
|
196
|
+
? message.isError
|
|
197
|
+
: undefined;
|
|
146
198
|
return {
|
|
147
199
|
role: 'tool',
|
|
148
200
|
content: messageContentToString(message.content),
|
|
149
|
-
tool_blocks: [
|
|
150
|
-
|
|
201
|
+
tool_blocks: [
|
|
202
|
+
{
|
|
203
|
+
type: 'tool_result',
|
|
204
|
+
tool_call_id: message.toolCallId,
|
|
205
|
+
tool_name: message.toolName,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
...(isError !== undefined ? { is_error: isError } : {}),
|
|
209
|
+
...(status !== undefined ? { status } : {}),
|
|
151
210
|
};
|
|
152
211
|
});
|
|
153
212
|
}
|
|
@@ -164,7 +223,9 @@ export function buildRoutingRequest(
|
|
|
164
223
|
request_id: randomUUID(),
|
|
165
224
|
session_id: sessionId,
|
|
166
225
|
prompt_text: extractPromptText(context.messages),
|
|
167
|
-
messages: mapContextMessages(context.messages
|
|
226
|
+
messages: mapContextMessages(context.messages, {
|
|
227
|
+
includeThinking: process.env[INCLUDE_THINKING_ENV] === '1',
|
|
228
|
+
}),
|
|
168
229
|
turn_type: deriveTurnType(context.messages),
|
|
169
230
|
estimated_input_tokens: estimateInputTokens(context, options),
|
|
170
231
|
...lifecycleFlags,
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Auto-model router middleware for the [pi](https://pi.dev) coding agent.**
|
|
4
4
|
|
|
5
|
-
> **v0.
|
|
5
|
+
> Current release: **v0.16.2** (mirrors `package.json`; SemVer `0.y.z`). The public API and routing behavior may change until `1.0.0`.
|
|
6
6
|
|
|
7
7
|
pi-smart-router intercepts every LLM inference request and dynamically routes it to the optimal execution engine — balancing cost, capability, latency, and time-to-first-token (TTFT) — without requiring you to manually pick a model for each turn.
|
|
8
8
|
|
|
@@ -18,8 +18,8 @@ pi-smart-router intercepts every LLM inference request and dynamically routes it
|
|
|
18
18
|
```text
|
|
19
19
|
request → hardware probe → loop escalation → turn envelope → context-fit gate
|
|
20
20
|
→ low-intensity tier gate → session pin → deterministic triage
|
|
21
|
-
→ local zero-tier →
|
|
22
|
-
→ context overflow fallback
|
|
21
|
+
→ local zero-tier → triage cloud fallback → HyDRA embedding matcher
|
|
22
|
+
→ safe cloud default → context overflow fallback
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
The pipeline runs **12 stages sequentially with early exit** — the moment any stage reaches a routing decision, subsequent stages are skipped. Every decision includes the stage name, reason code, candidates considered, estimated cost, and routing latency for full observability.
|
|
@@ -34,6 +34,7 @@ The pipeline runs **12 stages sequentially with early exit** — the moment any
|
|
|
34
34
|
| Session Pin | <1ms | Returns pinned model if session has one; breaks pin on compaction or overflow |
|
|
35
35
|
| Deterministic Triage | <5ms | Aho-Corasick keyword scan + cyclomatic complexity analysis |
|
|
36
36
|
| Local Zero-Tier | <15ms | Pings LM Studio + Ollama in parallel; routes locally when eligible |
|
|
37
|
+
| Triage Cloud Fallback | <2ms | Trivial prompts not claimed locally route to the first healthy economical-cloud model |
|
|
37
38
|
| HyDRA Matcher | 80-120ms | ONNX embeddings, 3D requirement projection, shortfall gate, multi-objective scoring |
|
|
38
39
|
| Safe Cloud Default | — | First healthy economical-cloud model (context-fit aware) |
|
|
39
40
|
| Context Overflow Fallback | — | Escalates to largest-fit model when economical tiers cannot fit |
|
|
@@ -258,6 +259,10 @@ After typing `/smart-router ` (with a trailing space), press **TAB** to see subc
|
|
|
258
259
|
npm run verify:ci
|
|
259
260
|
```
|
|
260
261
|
|
|
262
|
+
## Concurrency contract
|
|
263
|
+
|
|
264
|
+
`RouterPipeline.route()` calls on a single router instance are **single-flight**: concurrent calls are serialized internally (SP-230, [#141](https://github.com/beettlle/pi-smart-router/issues/141)). The pipeline keeps per-route transient state on instance fields while stages run, so overlapping executions are queued rather than interleaved — each queued call waits at most one routing latency. This applies to `createRouter()` / `createRouterFromFleet()` handles: a shared `router.dispatch` is safe to call concurrently, and serialization does not change routing policy outcomes. For parallel routing throughput, create separate router instances.
|
|
265
|
+
|
|
261
266
|
## Fleet behavior
|
|
262
267
|
|
|
263
268
|
When you use `smart-router/auto`, the extension does **not** read `config/models.yaml`. Instead:
|
|
@@ -949,7 +954,7 @@ Contributors must run `npm run build` before publishing or consuming the library
|
|
|
949
954
|
| `npm run release:check` | Pre-release gate: `verify:ci` + consumer pack + Tier 0 functional smoke |
|
|
950
955
|
| `npm run release:functional-smoke` | Tier 0 functional smoke: calibration verify (`--skip-embed`), benchmark profiles, release gate assertions |
|
|
951
956
|
| `npm run release:consumer-pack` | Pack tarball and verify production dependencies resolve (catches missing runtime deps) |
|
|
952
|
-
| `npm run verify:ci` | Full CI parity: build, typecheck, lint, test, coverage |
|
|
957
|
+
| `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)) |
|
|
953
958
|
| `npm run typecheck` | TypeScript strict mode check (`tsc --noEmit`) |
|
|
954
959
|
| `npm test` | Run test suite (`vitest run`) |
|
|
955
960
|
| `npm run coverage:check` | Tests with line-coverage thresholds |
|
|
@@ -1008,7 +1013,9 @@ npm run routing:eval-replay
|
|
|
1008
1013
|
npm run routing:twinrouterbench:full-track
|
|
1009
1014
|
```
|
|
1010
1015
|
|
|
1011
|
-
**CI smoke:** `.github/workflows/eval-harness-smoke.yml` runs on PRs that touch eval scripts, fixtures, or the workflow. It executes `routing:eval-harness:smoke`, `routing:eval-harness:corpus-smoke`, and eval unit tests — fast, offline, no provider network calls. Job timeout stays at 10 minutes. The optional full-track nightly (`.github/workflows/twinrouterbench-full-nightly.yml`, `schedule` + `workflow_dispatch` only) is **not** on `pull_request` and must not be configured as a required status check — failures there do not gate PR CI or `release:functional-smoke`.
|
|
1016
|
+
**CI smoke:** `.github/workflows/eval-harness-smoke.yml` runs on PRs that touch eval scripts, fixtures, **any `src/**` or `.pi/extensions/smart-router/**` change**, or the workflow. It executes `routing:eval-harness:smoke`, `routing:eval-harness:corpus-smoke`, and eval unit tests — fast, offline, no provider network calls. Job timeout stays at 10 minutes. The optional full-track nightly (`.github/workflows/twinrouterbench-full-nightly.yml`, `schedule` + `workflow_dispatch` only) is **not** on `pull_request` and must not be configured as a required status check — failures there do not gate PR CI or `release:functional-smoke`.
|
|
1017
|
+
|
|
1018
|
+
**Calibration verify:** `.github/workflows/calibration-verify.yml` runs on PRs that touch calibration config/scripts (`config/routing-calibration.json*`, `config/p-success-weights.json`, `scripts/train-routing-calibration.ts`, `scripts/verify-routing-calibration.ts`, `scripts/lib/isotonic-calibrator.ts`, `scripts/lib/oats-centroid-refinement.ts`) **or calibration-consuming routing code** (`src/domain/routing/**`, `src/domain/pipeline/**`, `src/domain/types/**`, `src/cli/**`). It builds the library and verifies `config/routing-calibration.json.example` against benchmark prompts via `npm run routing:verify-calibration`.
|
|
1012
1019
|
|
|
1013
1020
|
**TwinRouterBench static track:** import step-level router-visible prefixes with execution-verified target tiers (`track: "static"`). The adapter in `scripts/eval/twinrouterbench-adapter.ts` converts static track records into native eval fixtures for the three-track harness. See `docs/gemini-research.md` §9 for methodology context.
|
|
1014
1021
|
|
|
@@ -1178,6 +1185,21 @@ npm run routing:verify-benchmark-profiles
|
|
|
1178
1185
|
|
|
1179
1186
|
Tag-triggered publish via GitHub Actions (requires `NPMSECRET` repository secret). pi.dev gallery listing syncs automatically from npm (`pi-package` keyword); no separate submit step.
|
|
1180
1187
|
|
|
1188
|
+
#### PR and pre-release quality gate set
|
|
1189
|
+
|
|
1190
|
+
Operators should treat the following as the full gate set before merging routing changes and before tagging a release ([#135](https://github.com/beettlle/pi-smart-router/issues/135)):
|
|
1191
|
+
|
|
1192
|
+
| Gate | Workflow / command | Runs when |
|
|
1193
|
+
|------|--------------------|-----------|
|
|
1194
|
+
| Build / typecheck / lint / coverage | `.github/workflows/ci.yml` (`npm run verify:ci`) | Every PR and push to `main` |
|
|
1195
|
+
| Eval harness smoke (offline) | `.github/workflows/eval-harness-smoke.yml` | PRs touching `scripts/eval/**`, `tests/eval/**`, **`src/**`**, **`.pi/extensions/smart-router/**`**, `package.json`, or the workflow |
|
|
1196
|
+
| Calibration verify | `.github/workflows/calibration-verify.yml` | PRs touching calibration config/scripts or calibration-consuming routing code (`src/domain/routing/**`, `src/domain/pipeline/**`, `src/domain/types/**`, `src/cli/**`) |
|
|
1197
|
+
| Benchmark profile smoke | `.github/workflows/benchmark-profile-refresh.yml` (`routing:verify-benchmark-profiles`) | PRs touching fixtures / ingest / profiles |
|
|
1198
|
+
| Pre-release functional smoke | `npm run release:check` (Tier 0: calibration `--skip-embed` + benchmark profiles + release-gate assertions) | Operator-run before `npm version` / tag |
|
|
1199
|
+
| TwinRouterBench full track | `.github/workflows/twinrouterbench-full-nightly.yml` | Nightly / manual only — **never** a required PR check |
|
|
1200
|
+
|
|
1201
|
+
Required-check configuration for branch protection is a human-operator repo-settings decision; the workflow path filters above guarantee the jobs **run** on routing code edits regardless of which subset the operator marks required.
|
|
1202
|
+
|
|
1181
1203
|
**Scope composition:** use `/skill:router-release-operator` for themed release planning (not open-ended backlog cycles). **Patch** = docs + bugfixes only; **minor** = new capability (1–3 related issues under one theme). Budgets and audit rules: [`skills/router-release-operator/references/release-profiles.md`](skills/router-release-operator/references/release-profiles.md).
|
|
1182
1204
|
|
|
1183
1205
|
**Tier 0 functional smoke** (`release:functional-smoke`) runs before tag publish and chains:
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"livecodebench": "https://livecodebench.github.io/leaderboard.html",
|
|
8
8
|
"bfcl": "https://gorilla.cs.berkeley.edu/leaderboard.html"
|
|
9
9
|
},
|
|
10
|
-
"scrape_date": "2026-08-
|
|
11
|
-
"catalog_freeze_date": "2026-08-
|
|
10
|
+
"scrape_date": "2026-08-22",
|
|
11
|
+
"catalog_freeze_date": "2026-08-22"
|
|
12
12
|
},
|
|
13
13
|
"aliases": {
|
|
14
14
|
"anthropic/claude-opus-4": "claude-opus-4-5",
|
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
"max_messages": 12,
|
|
59
59
|
"max_tokens": 16384,
|
|
60
60
|
"exclude_execution_history": true
|
|
61
|
+
},
|
|
62
|
+
"global_timeout_ms": 120000,
|
|
63
|
+
"sub_call_timeout_ms": 30000,
|
|
64
|
+
"_timeouts_documentation": {
|
|
65
|
+
"global_timeout_ms": "Global cap (ms) for the whole delegate stage; mirrors llm-use WORKER_GLOBAL_TIMEOUT. Default 120000 (SP-213, #120).",
|
|
66
|
+
"sub_call_timeout_ms": "Per-call cap (ms) for each delegate sub-call worker; mirrors llm-use WORKER_CALL_TIMEOUT. Default 30000 (SP-213, #120)."
|
|
61
67
|
}
|
|
62
68
|
},
|
|
63
69
|
"local_zero": {
|
|
@@ -143,8 +143,34 @@ export declare class RouterPipeline {
|
|
|
143
143
|
private currentPrewarmOutcome;
|
|
144
144
|
/** Lazily created session-scoped prewarm guard (acceptance state spans routes). */
|
|
145
145
|
private prewarmGuardInstance;
|
|
146
|
+
/**
|
|
147
|
+
* Single-flight serialization tail (SP-230, #141).
|
|
148
|
+
*
|
|
149
|
+
* Concurrency contract: `route()` calls on one RouterPipeline instance are
|
|
150
|
+
* serialized — a concurrent caller queues behind the in-flight call. The
|
|
151
|
+
* pipeline keeps per-route transient state on instance fields (the
|
|
152
|
+
* `current*` / `activeFleet` / `fullFleet` members above), which every stage
|
|
153
|
+
* reads and writes; overlapping route() executions would race on that state
|
|
154
|
+
* and corrupt routing decisions (e.g. a second call's reset swapping
|
|
155
|
+
* `activeFleet` mid-flight for the first). Routing is a fast, bounded,
|
|
156
|
+
* in-memory computation, so serialization costs at most one routing latency
|
|
157
|
+
* of queuing and never changes routing policy outcomes.
|
|
158
|
+
*
|
|
159
|
+
* Safety notes:
|
|
160
|
+
* - No reentrancy: nothing on a route() execution path awaits another
|
|
161
|
+
* route()/dispatch() on the same instance, so the chain cannot deadlock.
|
|
162
|
+
* - The tail is chained with a rejection handler so a rejected call (the
|
|
163
|
+
* zero-crash catch makes this defensive-only) cannot wedge the queue.
|
|
164
|
+
*/
|
|
165
|
+
private routeTail;
|
|
146
166
|
constructor(fleet: readonly ModelProfile[], options?: PipelineOptions);
|
|
167
|
+
/**
|
|
168
|
+
* Route a request through the pipeline. Concurrent calls are serialized
|
|
169
|
+
* (single-flight) — see `routeTail` for the concurrency contract (SP-230).
|
|
170
|
+
*/
|
|
147
171
|
route(request: RoutingRequest, fleetOverride?: readonly ModelProfile[]): Promise<RoutingDecision>;
|
|
172
|
+
/** Exclusive-route body — never invoke concurrently; see `route()` (SP-230). */
|
|
173
|
+
private routeExclusive;
|
|
148
174
|
/**
|
|
149
175
|
* SP-080: move Google/Gemini profiles to the end of the fleet when prior tool
|
|
150
176
|
* calls exist so tier `.find()` passes prefer non-Gemini models first.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router-pipeline.d.ts","sourceRoot":"","sources":["../../../src/domain/pipeline/router-pipeline.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"router-pipeline.d.ts","sourceRoot":"","sources":["../../../src/domain/pipeline/router-pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -157,6 +157,26 @@ export class RouterPipeline {
|
|
|
157
157
|
currentPrewarmOutcome = null;
|
|
158
158
|
/** Lazily created session-scoped prewarm guard (acceptance state spans routes). */
|
|
159
159
|
prewarmGuardInstance = null;
|
|
160
|
+
/**
|
|
161
|
+
* Single-flight serialization tail (SP-230, #141).
|
|
162
|
+
*
|
|
163
|
+
* Concurrency contract: `route()` calls on one RouterPipeline instance are
|
|
164
|
+
* serialized — a concurrent caller queues behind the in-flight call. The
|
|
165
|
+
* pipeline keeps per-route transient state on instance fields (the
|
|
166
|
+
* `current*` / `activeFleet` / `fullFleet` members above), which every stage
|
|
167
|
+
* reads and writes; overlapping route() executions would race on that state
|
|
168
|
+
* and corrupt routing decisions (e.g. a second call's reset swapping
|
|
169
|
+
* `activeFleet` mid-flight for the first). Routing is a fast, bounded,
|
|
170
|
+
* in-memory computation, so serialization costs at most one routing latency
|
|
171
|
+
* of queuing and never changes routing policy outcomes.
|
|
172
|
+
*
|
|
173
|
+
* Safety notes:
|
|
174
|
+
* - No reentrancy: nothing on a route() execution path awaits another
|
|
175
|
+
* route()/dispatch() on the same instance, so the chain cannot deadlock.
|
|
176
|
+
* - The tail is chained with a rejection handler so a rejected call (the
|
|
177
|
+
* zero-crash catch makes this defensive-only) cannot wedge the queue.
|
|
178
|
+
*/
|
|
179
|
+
routeTail = Promise.resolve();
|
|
160
180
|
constructor(fleet, options) {
|
|
161
181
|
this.fleet = fleet;
|
|
162
182
|
this.options = options ?? {};
|
|
@@ -175,7 +195,20 @@ export class RouterPipeline {
|
|
|
175
195
|
{ name: 'context_overflow_fallback', run: this.contextOverflowFallback.bind(this) },
|
|
176
196
|
];
|
|
177
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Route a request through the pipeline. Concurrent calls are serialized
|
|
200
|
+
* (single-flight) — see `routeTail` for the concurrency contract (SP-230).
|
|
201
|
+
*/
|
|
178
202
|
async route(request, fleetOverride) {
|
|
203
|
+
const queued = this.routeTail.then(() => this.routeExclusive(request, fleetOverride));
|
|
204
|
+
// Keep the tail alive even if a call rejects (defensive: routeExclusive
|
|
205
|
+
// catches stage errors, but a throw outside that catch must not wedge the
|
|
206
|
+
// queue for subsequent callers).
|
|
207
|
+
this.routeTail = queued.then(() => undefined, () => undefined);
|
|
208
|
+
return queued;
|
|
209
|
+
}
|
|
210
|
+
/** Exclusive-route body — never invoke concurrently; see `route()` (SP-230). */
|
|
211
|
+
async routeExclusive(request, fleetOverride) {
|
|
179
212
|
const start = Date.now();
|
|
180
213
|
this.activeFleet = this.prioritizeFleetForToolHistory(fleetOverride ?? this.fleet, request);
|
|
181
214
|
this.fullFleet = this.activeFleet;
|
|
@@ -1174,6 +1207,12 @@ export class RouterPipeline {
|
|
|
1174
1207
|
};
|
|
1175
1208
|
}
|
|
1176
1209
|
logExpectedCostExplain(pSuccessCheap, alpha, selection, calibration) {
|
|
1210
|
+
// SP-223 / #138: gate stdout explain behind SMART_ROUTER_LOG_ROUTING —
|
|
1211
|
+
// the full payload is already captured in decision features/telemetry, so
|
|
1212
|
+
// default runs must not flood stdout on every eligible route.
|
|
1213
|
+
if (process.env.SMART_ROUTER_LOG_ROUTING !== '1') {
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1177
1216
|
console.info('Expected-cost tier gate', {
|
|
1178
1217
|
reason: selection.reasonCode,
|
|
1179
1218
|
p_success_cheap: pSuccessCheap,
|