pi-smart-router 0.19.0 → 0.19.2
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.
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type Context,
|
|
7
7
|
type Model,
|
|
8
8
|
type SimpleStreamOptions,
|
|
9
|
-
streamSimple as
|
|
9
|
+
streamSimple as compatDelegateStream,
|
|
10
10
|
} from '@earendil-works/pi-ai/compat';
|
|
11
11
|
|
|
12
12
|
import { parseAssistantMessageError } from '../../../src/infrastructure/delegation/provider-error.js';
|
|
@@ -21,9 +21,55 @@ import {
|
|
|
21
21
|
type FailoverNoticeInfo,
|
|
22
22
|
type FlushDelegatedEventsOptions,
|
|
23
23
|
} from './delegation-runtime.js';
|
|
24
|
-
import type { StreamDelegationDeps } from './types.js';
|
|
24
|
+
import type { DelegateStreamFn, StreamDelegationDeps } from './types.js';
|
|
25
25
|
import { throwIfAborted } from './utils.js';
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Minimal structural view of pi's composed provider (pi-coding-agent
|
|
29
|
+
* `ModelRegistry.getProvider()` → `composeModelProvider`). Typed structurally
|
|
30
|
+
* because `getProvider` only exists on newer pi-coding-agent versions (0.84+);
|
|
31
|
+
* the extension must keep loading against older ones.
|
|
32
|
+
*/
|
|
33
|
+
interface ComposedProviderLike {
|
|
34
|
+
streamSimple: DelegateStreamFn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type RegistryWithProviders = StreamDelegationDeps['modelRegistry'] & {
|
|
38
|
+
getProvider?: (providerId: string) => ComposedProviderLike | undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the stream entrypoint for a delegation target (SP-238, #160).
|
|
43
|
+
*
|
|
44
|
+
* Priority:
|
|
45
|
+
* 1. Explicit `deps.delegateStream` injection (tests / operator override).
|
|
46
|
+
* 2. The **composed provider** from `modelRegistry.getProvider(model.provider)` —
|
|
47
|
+
* the same path pi's own agent loop delegates through. Its `streamSimple`
|
|
48
|
+
* checks extension-registered providers (`extension.streamSimple` when
|
|
49
|
+
* `model.api === extension.api`) before falling back to pi-ai's built-in
|
|
50
|
+
* registry, so custom-API models (claude-bridge et al.) resolve correctly.
|
|
51
|
+
* 3. Bare `pi-ai/compat streamSimple` — dispatches only on `model.api` against
|
|
52
|
+
* pi-ai's private built-in registry; used when no composed provider exists
|
|
53
|
+
* (older pi-coding-agent, or providers never composed through ModelRuntime).
|
|
54
|
+
*/
|
|
55
|
+
function resolveDelegateStream(
|
|
56
|
+
targetModel: Model<Api>,
|
|
57
|
+
deps: StreamDelegationDeps,
|
|
58
|
+
): DelegateStreamFn {
|
|
59
|
+
if (deps.delegateStream) {
|
|
60
|
+
return deps.delegateStream;
|
|
61
|
+
}
|
|
62
|
+
const registry = deps.modelRegistry as RegistryWithProviders;
|
|
63
|
+
const provider =
|
|
64
|
+
typeof registry.getProvider === 'function'
|
|
65
|
+
? registry.getProvider(targetModel.provider)
|
|
66
|
+
: undefined;
|
|
67
|
+
if (provider && typeof provider.streamSimple === 'function') {
|
|
68
|
+
return (model, context, options) => provider.streamSimple(model, context, options);
|
|
69
|
+
}
|
|
70
|
+
return compatDelegateStream;
|
|
71
|
+
}
|
|
72
|
+
|
|
27
73
|
function isTerminalEvent(
|
|
28
74
|
event: AssistantMessageEvent,
|
|
29
75
|
): event is Extract<AssistantMessageEvent, { type: 'done' | 'error' }> {
|
|
@@ -52,7 +98,7 @@ export async function collectDelegatedStream(
|
|
|
52
98
|
options,
|
|
53
99
|
headroomContext,
|
|
54
100
|
);
|
|
55
|
-
const delegateStream = deps
|
|
101
|
+
const delegateStream = resolveDelegateStream(targetModel, deps);
|
|
56
102
|
const inner = delegateStream(targetModel, context, delegationOptions);
|
|
57
103
|
const events: AssistantMessageEvent[] = [];
|
|
58
104
|
let finalMessage: AssistantMessage | undefined;
|
|
@@ -119,7 +165,7 @@ export async function pipeDelegatedStream(
|
|
|
119
165
|
options,
|
|
120
166
|
headroomContext,
|
|
121
167
|
);
|
|
122
|
-
const delegateStream = deps
|
|
168
|
+
const delegateStream = resolveDelegateStream(targetModel, deps);
|
|
123
169
|
const inner = delegateStream(targetModel, context, delegationOptions);
|
|
124
170
|
const events: AssistantMessageEvent[] = [];
|
|
125
171
|
let finalMessage: AssistantMessage | undefined;
|
|
@@ -57,7 +57,12 @@ export interface StreamDelegationDeps {
|
|
|
57
57
|
/** Cheap scope fingerprint check before each routed turn. */
|
|
58
58
|
ensureFleetFresh?: () => Promise<void>;
|
|
59
59
|
readonly executionLedger: ExecutionLedger;
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Injectable stream override for tests; when unset, delegation resolves the
|
|
62
|
+
* stream through `modelRegistry.getProvider(...)` (composed provider, which
|
|
63
|
+
* knows extension-registered custom APIs — SP-238, #160), falling back to
|
|
64
|
+
* pi-ai/compat `streamSimple` when no composed provider exists.
|
|
65
|
+
*/
|
|
61
66
|
delegateStream?: DelegateStreamFn;
|
|
62
67
|
/** Injectable planning delegate sub-call; production uses frontier stream delegate. */
|
|
63
68
|
spawnPlanningDelegate?: PlanningDelegateSpawnFn;
|
package/README.md
CHANGED
|
@@ -263,6 +263,50 @@ npm run verify:ci
|
|
|
263
263
|
|
|
264
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
265
|
|
|
266
|
+
## Library vs extension
|
|
267
|
+
|
|
268
|
+
pi-smart-router ships in two shapes, and they are **not** feature-identical ([#153](https://github.com/beettlle/pi-smart-router/issues/153)):
|
|
269
|
+
|
|
270
|
+
| Path | What you get |
|
|
271
|
+
|------|--------------|
|
|
272
|
+
| **Pi extension** (`pi install npm:pi-smart-router`, or project-local `.pi/extensions/smart-router/` when developing from clone) | The full product — the routing pipeline **plus** the stream-level behaviors below |
|
|
273
|
+
| **npm library** (`createRouter()` / `createRouterFromFleet()` / `GatewayDispatch`) | The routing core — the 12-stage pipeline, fleet mapping, telemetry, and gateway health/failover *selection*. Stream-level behaviors are stubbed or left to your embedder loop |
|
|
274
|
+
|
|
275
|
+
### Extension-only capabilities
|
|
276
|
+
|
|
277
|
+
These behaviors run in `.pi/extensions/smart-router/` and have **no equivalent in the library API**:
|
|
278
|
+
|
|
279
|
+
| Capability | Extension implementation | Library status |
|
|
280
|
+
|------------|--------------------------|----------------|
|
|
281
|
+
| **Planning delegate spawn** — cache-preserving ephemeral frontier sub-call on planning turns, with observation injection and bounded timeouts (SP-144, SP-213 / [#71](https://github.com/beettlle/pi-smart-router/issues/71), [#120](https://github.com/beettlle/pi-smart-router/issues/120)) | `.pi/extensions/smart-router/planning-delegate.ts` — compressed-context sub-call via `streamSimple`; falls back to direct frontier with a documented `fallback_reason` | The pipeline still emits `planning_delegate` decisions with delegate model and compressed limits, but **nothing spawns the delegate** — an embedder must implement the sub-call itself or accept direct-frontier routing |
|
|
282
|
+
| **Stream failover loop** — live provider-error failover across candidate models with user-facing notices (atomic state machine, [#33](https://github.com/beettlle/pi-smart-router/issues/33)) | `.pi/extensions/smart-router/route-and-delegate.ts` (~L377–595) — retries stream delegation across alternates, emits failover notices, ends in SP-226 fail-open safe default | `GatewayDispatch.selectFailover()` only **selects** an alternate model; no stream retry loop runs. The embedder owns iterating over failures and re-dispatching |
|
|
283
|
+
| **Output headroom escalation** — exclude failover candidates whose context window cannot fit input plus the required output floor (SP-108) | `route-and-delegate.ts` + `.pi/extensions/smart-router/delegation-runtime.ts` — per-attempt `computeOutputHeadroom` checks; `headroomExcludedModelIds` accumulate across the failover loop | `src/domain/delegation/output-headroom.ts` ships the helper, but **no library caller wires it** into `GatewayDispatch.dispatch()` — the embedder must apply it per attempt |
|
|
284
|
+
| **Cursor quota handling** — subscription-quota exhaustion detection and failover to `cursor/auto` or economical API models with `cursor_quota_exhausted` telemetry (SP-097 / [#70](https://github.com/beettlle/pi-smart-router/issues/70)) | The extension stream loop catches quota errors from live streams and drives `selectFailover` reactively | Detection and failover *selection* exist in `src/infrastructure/gateway/gateway-dispatch.ts` (`isCursorQuotaExhaustedError`, per-model quota tracking), but the **reactive trigger lives in the extension's stream loop** — library dispatch alone does not observe provider stream errors |
|
|
285
|
+
|
|
286
|
+
### The middleware is a lifecycle stub, not a router
|
|
287
|
+
|
|
288
|
+
`createPiRouterMiddleware()` / `RouterHandle.register()` (`src/api/middleware/pi-router-middleware.ts`) registers **lifecycle hooks only** — compaction flags and `model_select` overrides consumed when building the next routing request. It does **not** intercept LLM streams, route requests, or delegate inference. The production stream path lives in the pi extension (`route-and-delegate.ts`, `stream-delegation.ts`, `delegate-stream.ts`), not in the npm-exported middleware. Treat `middleware` as a flag registrar; routing happens through your call to `router.dispatch.dispatch()` or the extension's stream path.
|
|
289
|
+
|
|
290
|
+
### Recommended integration path
|
|
291
|
+
|
|
292
|
+
- **pi users:** install the **extension** (`pi install npm:pi-smart-router`). It is the full product — everything in the table above works out of the box, including failover, delegate spawn, headroom escalation, and quota reaction.
|
|
293
|
+
- **npm embedders:** you get the **routing core** (12-stage pipeline, fleet mapping, telemetry, gateway health tracking, failover *selection*). Plan to implement your own stream delegation, failover iteration, headroom checks, and planning-delegate spawn around the decisions the pipeline returns — or track [#149](https://github.com/beettlle/pi-smart-router/issues/149) (**extension public facade**), the migration plan for exposing the extension's stream/delegation surface as supported library API so this gap closes over time. Until #149 lands, the extension modules also import `src/**` internals directly, so deep imports into `src/` are not a stable API.
|
|
294
|
+
|
|
295
|
+
```text
|
|
296
|
+
pi extension path (full product) npm library path (routing core)
|
|
297
|
+
──────────────────────────────── ───────────────────────────────
|
|
298
|
+
pi (host agent) your host application
|
|
299
|
+
└─ .pi/extensions/smart-router/ └─ createRouter() / createRouterFromFleet()
|
|
300
|
+
├─ routing pipeline (src/) ════════ ├─ routing pipeline (src/) ← shared core
|
|
301
|
+
├─ stream failover loop ✗ ├─ GatewayDispatch: health tracking,
|
|
302
|
+
├─ planning delegate spawn ✗ │ failover selection only
|
|
303
|
+
├─ output headroom escalation ✗ ├─ lifecycle middleware (stub: hooks only)
|
|
304
|
+
└─ cursor quota failover ✗ └─ embedder implements: stream loop,
|
|
305
|
+
delegate spawn, headroom checks, quota reaction
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`✗` = capability exists only on the extension path today; [#149](https://github.com/beettlle/pi-smart-router/issues/149) is the plan to close the gap.
|
|
309
|
+
|
|
266
310
|
## Fleet behavior
|
|
267
311
|
|
|
268
312
|
When you use `smart-router/auto`, the extension does **not** read `config/models.yaml`. Instead:
|
|
@@ -1275,19 +1319,21 @@ Confirm https://pi.dev/packages/pi-smart-router shows the new version (may lag n
|
|
|
1275
1319
|
|
|
1276
1320
|
## Develop with pi-spine (agent models)
|
|
1277
1321
|
|
|
1278
|
-
This repo is developed with [pi-spine](https://github.com/beettlle/pi-spine) batches. Agent model pins live in [`.spine/spine-config.json`](.spine/spine-config.json) under `agents.*`. Use **canonical** `provider/model` ids from `pi --list-models` (not TUI labels like `glm-5.
|
|
1322
|
+
This repo is developed with [pi-spine](https://github.com/beettlle/pi-spine) batches. Agent model pins live in [`.spine/spine-config.json`](.spine/spine-config.json) under `agents.*`. Use **canonical** `provider/model` ids from `pi --list-models` (not TUI labels like `glm-5.3 [zai]`). Run `spine doctor` before real-pi batches.
|
|
1279
1323
|
|
|
1280
1324
|
Named profiles (`agents.profiles` + `agents.activeProfile`) and `agents.escalatePolicy` are configured in spine-config ([pi-spine#216](https://github.com/beettlle/pi-spine/issues/216) / SP-664). Live stack resolves from `activeProfile` over the base `agents` block. Hybrid cost/quality recipes are documented upstream in [pi-spine#210](https://github.com/beettlle/pi-spine/issues/210).
|
|
1281
1325
|
|
|
1326
|
+
**Sale window (through 2026-09-09 UTC+8):** plan review and supervisor use `zai/glm-5.3-flash` (Z.AI 50% promo). After the promo, revert plan to `google/gemini-flash-latest` and supervisor to `google/gemini-flash-lite-latest` if desired.
|
|
1327
|
+
|
|
1282
1328
|
### Default profile (`activeProfile: "default"`)
|
|
1283
1329
|
|
|
1284
1330
|
| Role | Model | Thinking |
|
|
1285
1331
|
|------|--------|----------|
|
|
1286
|
-
| Worker | `
|
|
1287
|
-
| Plan review | `
|
|
1288
|
-
| Code review | `kimi-coding/kimi-
|
|
1332
|
+
| Worker | `kimi-coding/k3` | `high` |
|
|
1333
|
+
| Plan review | `zai/glm-5.3-flash` | `low` |
|
|
1334
|
+
| Code review | `kimi-coding/kimi-for-coding` | `high` |
|
|
1289
1335
|
| Final review | `google/gemini-3.1-pro-preview` | `high` |
|
|
1290
|
-
| Supervisor | `
|
|
1336
|
+
| Supervisor | `zai/glm-5.3-flash` | `off` |
|
|
1291
1337
|
|
|
1292
1338
|
### When to escalate (hard packets / sticky failures)
|
|
1293
1339
|
|
|
@@ -1297,12 +1343,12 @@ Escalate when:
|
|
|
1297
1343
|
- The worker stalls or oscillates on multi-file design
|
|
1298
1344
|
- The packet needs deeper reasoning than the default stack delivered
|
|
1299
1345
|
|
|
1300
|
-
### Tier 1 —
|
|
1346
|
+
### Tier 1 — budget escalate
|
|
1301
1347
|
|
|
1302
|
-
Switches the worker to `
|
|
1348
|
+
Switches the worker to `zai/glm-5.3` (same list price as 5.2; plan/supervisor stay on sale Flash).
|
|
1303
1349
|
|
|
1304
1350
|
```bash
|
|
1305
|
-
spine settings set agents.activeProfile
|
|
1351
|
+
spine settings set agents.activeProfile budget
|
|
1306
1352
|
spine batch retry <SP-ID>
|
|
1307
1353
|
# restore:
|
|
1308
1354
|
spine settings set agents.activeProfile default
|
|
@@ -1313,7 +1359,7 @@ spine settings set agents.activeProfile default
|
|
|
1313
1359
|
| Role | Model | Thinking |
|
|
1314
1360
|
|------|--------|----------|
|
|
1315
1361
|
| Worker | `kimi-coding/k3` | `high` |
|
|
1316
|
-
| Plan | `kimi-coding/kimi-
|
|
1362
|
+
| Plan | `kimi-coding/kimi-for-coding` | `medium` |
|
|
1317
1363
|
| Code | `google/gemini-3.1-pro-preview` | `high` |
|
|
1318
1364
|
| Final | `google/gemini-3.1-pro-preview` | `high` |
|
|
1319
1365
|
|
|
@@ -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-29",
|
|
11
|
+
"catalog_freeze_date": "2026-08-29"
|
|
12
12
|
},
|
|
13
13
|
"aliases": {
|
|
14
14
|
"anthropic/claude-opus-4": "claude-opus-4-5",
|