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 defaultDelegateStream,
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.delegateStream ?? defaultDelegateStream;
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.delegateStream ?? defaultDelegateStream;
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
- /** Injectable for tests; production uses pi-ai streamSimple. */
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.2 [zai]`). Run `spine doctor` before real-pi batches.
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 | `zai/glm-5.2` | `high` |
1287
- | Plan review | `google/gemini-flash-latest` | `low` |
1288
- | Code review | `kimi-coding/kimi-k2-thinking` | `high` |
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 | `google/gemini-flash-lite-latest` | `off` |
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 — mid escalate
1346
+ ### Tier 1 — budget escalate
1301
1347
 
1302
- Switches the worker to `kimi-coding/kimi-for-coding` (reviewer pins inherit default).
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 mid
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-k2-thinking` | `medium` |
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-28",
11
- "catalog_freeze_date": "2026-08-28"
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-smart-router",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "Auto-model router middleware for the pi.dev coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",