@kdejaeger/pi-model-router 0.4.0 → 0.4.1
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/README.md +94 -67
- package/docs/ARCHITECTURE.md +1 -1
- package/extensions/config.ts +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ pi -e ./extensions/index.ts
|
|
|
65
65
|
|
|
66
66
|
1. **Install** the package (see above).
|
|
67
67
|
|
|
68
|
-
2. **Create a config file** at `.pi/model-router.json` in your project:
|
|
68
|
+
2. **Create a config file** at `.pi/model-router.json` in your project (bigger example configs later in this document):
|
|
69
69
|
|
|
70
70
|
```json
|
|
71
71
|
{
|
|
@@ -114,9 +114,9 @@ Config is loaded from two locations and **merged**:
|
|
|
114
114
|
|
|
115
115
|
### Config Merging Order
|
|
116
116
|
|
|
117
|
-
Configs are merged: **
|
|
117
|
+
Configs are merged: **Base defaults <- Global config <- Project config**.
|
|
118
118
|
|
|
119
|
-
Project config values override global values, which override built-in defaults. Profiles are merged **deeply** -- if you define only a `high` tier override for a profile in your project config, the `medium` and `low` tiers are inherited from the global config
|
|
119
|
+
Project config values override global values, which override built-in defaults. Profiles are merged **deeply** -- if you define only a `high` tier override for a profile in your project config, the `medium` and `low` tiers are inherited from the global config.
|
|
120
120
|
|
|
121
121
|
**When no config file exists**, the router loads with an empty profile list and no active models. Create a `.pi/model-router.json` with at least one profile to use the router.
|
|
122
122
|
|
|
@@ -127,7 +127,7 @@ Project config values override global values, which override built-in defaults.
|
|
|
127
127
|
| Field | Type | Default | Description |
|
|
128
128
|
|---|---|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
129
129
|
| `debug` | `boolean` | `false` | Enable debug mode. Equivalent to running `/router debug on` at startup. |
|
|
130
|
-
| `classifierModels` | `string[]` | -- | Array of fast model refs (e.g. `["google/gemini-flash-latest"]`) used to classify user intent via LLM. Models are tried in order, providing fallback if one hits an error. When set, the classifier has final say on tier selection (gated by triggers below). Omit to
|
|
130
|
+
| `classifierModels` | `string[]` | -- | Array of fast model refs (e.g. `["google/gemini-flash-latest"]`) used to classify user intent via LLM. Models are tried in order, providing fallback if one hits an error. When set, the classifier has final say on tier selection (gated by triggers below). Omit to always default to `medium` tier (no automatic tier switching). |
|
|
131
131
|
| `classifierModelThinking` | `ThinkingLevel` | `off` | Reasoning/thinking level for the classifier model calls. Defaults to `off` (no extended reasoning) to keep calls fast and cheap. |
|
|
132
132
|
| `classifierRunOnceAfterToolCount` | `number` | `3` | Run the classifier once after this many tool continuations (only after the first user message of a turn). Default: 3. Set to 0 to disable. |
|
|
133
133
|
| `classifierRunAfterToolFailures` | `number` | `2` | Run the classifier after this many consecutive tool failures (counting from the tail of the current turn). Default: 2. |
|
|
@@ -146,7 +146,7 @@ Each profile defines three **tiers** (`high`, `medium`, `low`). Each tier config
|
|
|
146
146
|
| `thinking` | `ThinkingLevel` | -- | **Optional.** Reasoning/thinking level for this tier. |
|
|
147
147
|
| `fallbacks` | `string[]` | -- | **Optional.** Ordered list of fallback model refs. If the primary model fails, the router retries each fallback in sequence before surfacing an error. |
|
|
148
148
|
|
|
149
|
-
**Valid thinking levels** (from least to most reasoning): `off`, `minimal`, `low`, `medium`, `high`, `xhigh`
|
|
149
|
+
**Valid thinking levels** (from least to most reasoning): `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`
|
|
150
150
|
|
|
151
151
|
### Context Threshold Overrides
|
|
152
152
|
|
|
@@ -341,7 +341,7 @@ Each tier can define `fallbacks` -- an ordered list of alternative models. If th
|
|
|
341
341
|
|
|
342
342
|
When a fallback is used, `decision.isFallback` is set to `true` and shown in the status. The tier's configured thinking level (or runtime override) applies to all fallback models -- if a fallback doesn't support the requested level, pi silently clamps it.
|
|
343
343
|
|
|
344
|
-
If a model fails during a turn, the router retries it
|
|
344
|
+
If a model fails during a turn, the router retries it **once** (2 total attempts) before moving to the next fallback in the chain.
|
|
345
345
|
|
|
346
346
|
### Image-Aware Auto-Routing
|
|
347
347
|
|
|
@@ -357,7 +357,7 @@ When using Google models with thinking enabled, tool-result continuations requir
|
|
|
357
357
|
|
|
358
358
|
The router reports the **largest context window across all models in a profile** (scanning all tiers and their fallbacks for the maximum). When routing to a model with a smaller window, the router trims oldest messages (preserving the system prompt and the most recent message) to fit within the target model's limit.
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
Conservative estimation: **4 characters = 1 token**.
|
|
361
361
|
|
|
362
362
|
This is a rough last-resort cut, not a replacement for pi's built-in session compaction (`/compact`).
|
|
363
363
|
|
|
@@ -365,91 +365,118 @@ This is a rough last-resort cut, not a replacement for pi's built-in session com
|
|
|
365
365
|
|
|
366
366
|
**Persistent State:** Router state persists across agent restarts AND conversation branches via `pi.appendEntry` with a custom `router-state` entry type. Pins, debug mode, debug history, the last routing decision, and the last non-router model are all preserved. State is **branch-safe** -- different conversation branches maintain independent state using `sessionManager.getBranch()`.
|
|
367
367
|
|
|
368
|
-
**Status Line:** The router shows its status in the pi TUI status bar
|
|
368
|
+
**Status Line:** The router shows its status as a single line in the pi TUI status bar, e.g.:
|
|
369
369
|
```
|
|
370
|
-
|
|
371
|
-
Profile: balanced (active)
|
|
372
|
-
Pin: none
|
|
373
|
-
Route: medium -> google/gemini-flash-latest
|
|
370
|
+
⇋ medium -> google/gemini-flash-latest
|
|
374
371
|
```
|
|
375
372
|
|
|
376
|
-
The
|
|
377
|
-
- `[classifier]` — routed by the LLM classifier
|
|
378
|
-
|
|
373
|
+
The status line may show decision flags in brackets when applicable:
|
|
379
374
|
- `[fallback]` — a fallback model was used
|
|
380
375
|
- `[context]` — context threshold triggered an upgrade
|
|
381
376
|
|
|
382
|
-
**Debug History:** With `/router debug on`, routing decisions and classifier
|
|
377
|
+
**Debug History:** With `/router debug on`, routing decisions and classifier runs are logged with timestamps. View with `/router debug show` to see the routing history:
|
|
383
378
|
|
|
384
379
|
```
|
|
385
|
-
|
|
386
|
-
SKIP classifier (cont:2, fail:0)
|
|
380
|
+
Running router classifier — init(≥3), interval(%10) (cont:5) ...
|
|
387
381
|
```
|
|
388
382
|
|
|
389
|
-
The classifier
|
|
383
|
+
The classifier notification shows why the classifier was triggered (`cont` = tool-result continuations since the last user message, `fail` = consecutive recent tool failures).
|
|
390
384
|
```
|
|
391
|
-
[10:32:15 AM] high -> openai/gpt-5.4-pro (high) -
|
|
392
|
-
[10:33:42 AM] medium -> google/gemini-flash-latest (medium) -
|
|
393
|
-
[10:34:10 AM] low -> openai/gpt-5.4-nano (low) -
|
|
385
|
+
[10:32:15 AM] high -> openai/gpt-5.4-pro (high) - Classifier: multi-file architecture change across 4 services requires careful trade-off analysis.
|
|
386
|
+
[10:33:42 AM] medium -> google/gemini-flash-latest (medium) - Classifier: implementing a well-defined feature with clear acceptance criteria.
|
|
387
|
+
[10:34:10 AM] low -> openai/gpt-5.4-nano (low) - Classifier: simple field rename with no behavioral changes.
|
|
394
388
|
```
|
|
395
389
|
|
|
396
390
|
---
|
|
397
391
|
|
|
398
|
-
## Example
|
|
399
|
-
|
|
400
|
-
### Balanced (`balanced`)
|
|
392
|
+
## Example Configuration
|
|
401
393
|
|
|
402
394
|
```json
|
|
403
395
|
{
|
|
404
|
-
"
|
|
405
|
-
"
|
|
406
|
-
"
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
```
|
|
396
|
+
"debug": true,
|
|
397
|
+
"classifierModels": ["openrouter/deepseek/deepseek-v4-flash"],
|
|
398
|
+
"classifierModelThinking": "high",
|
|
399
|
+
"defaultContextThresholdPercent": 80,
|
|
400
|
+
"contextThresholdPercentOverrides": {
|
|
401
|
+
"opencode/deepseek-v4-flash-free": 60,
|
|
402
|
+
"openrouter/deepseek/deepseek-v4-flash": 60,
|
|
403
|
+
"openrouter/deepseek/deepseek-v4-pro": 50,
|
|
415
404
|
|
|
416
|
-
|
|
405
|
+
"openrouter/google/gemma-4-31b-it": 50,
|
|
406
|
+
"openrouter/google/gemini-3-flash-preview": 80,
|
|
407
|
+
"openrouter/google/gemini-3.5-flash": 80,
|
|
408
|
+
"openrouter/google/gemini-3.1-pro-preview": 18,
|
|
417
409
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
"profiles": {
|
|
421
|
-
"cheap": {
|
|
422
|
-
"high": { "model": "google/gemini-flash-latest", "thinking": "low" },
|
|
423
|
-
"medium": { "model": "openai/gpt-5.4-nano", "thinking": "off" },
|
|
424
|
-
"low": { "model": "google/gemini-flash-lite-latest", "thinking": "off" }
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
```
|
|
410
|
+
"openrouter/moonshotai/kimi-k2.6": 70,
|
|
411
|
+
"openrouter/moonshotai/kimi-k2.7-code": 60,
|
|
429
412
|
|
|
430
|
-
|
|
413
|
+
"openrouter/minimax/minimax-m3": 50,
|
|
431
414
|
|
|
432
|
-
|
|
433
|
-
{
|
|
434
|
-
"profiles": {
|
|
435
|
-
"deep": {
|
|
436
|
-
"high": { "model": "openai/o1-preview", "thinking": "xhigh" },
|
|
437
|
-
"medium": { "model": "openai/gpt-5.4-pro", "thinking": "medium" },
|
|
438
|
-
"low": { "model": "google/gemini-flash-latest", "thinking": "low" }
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
```
|
|
415
|
+
"openrouter/z-ai/glm-5.2": 50,
|
|
443
416
|
|
|
444
|
-
|
|
417
|
+
"openai-codex/gpt-5.4-mini": 60,
|
|
418
|
+
"openai-codex/gpt-5.5": 60,
|
|
419
|
+
"openai-codex/gpt-5.6-luna": 65,
|
|
420
|
+
"openai-codex/gpt-5.6-terra": 65,
|
|
421
|
+
"openai-codex/gpt-5.6-sol": 65,
|
|
422
|
+
"openrouter/openai/gpt-5.4-nano": 30,
|
|
445
423
|
|
|
446
|
-
|
|
447
|
-
|
|
424
|
+
"openrouter/anthropic/claude-opus-4.8": 65,
|
|
425
|
+
|
|
426
|
+
"openrouter/x-ai/grok-4.5": 50
|
|
427
|
+
},
|
|
448
428
|
"profiles": {
|
|
449
|
-
"
|
|
450
|
-
"high":
|
|
451
|
-
|
|
452
|
-
|
|
429
|
+
"cheap": {
|
|
430
|
+
"high": {
|
|
431
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "xhigh",
|
|
432
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
433
|
+
},
|
|
434
|
+
"medium": {
|
|
435
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "high",
|
|
436
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
437
|
+
},
|
|
438
|
+
"low": {
|
|
439
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "off",
|
|
440
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
"balanced": {
|
|
444
|
+
"high": {
|
|
445
|
+
"model": "openrouter/z-ai/glm-5.2", "thinking": "xhigh",
|
|
446
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
447
|
+
},
|
|
448
|
+
"medium": {
|
|
449
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "xhigh",
|
|
450
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
451
|
+
},
|
|
452
|
+
"low": {
|
|
453
|
+
"model": "openrouter/deepseek/deepseek-v4-flash", "thinking": "off",
|
|
454
|
+
"fallbacks": ["openrouter/minimax/minimax-m3"]
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
"expensive": {
|
|
458
|
+
"high": {
|
|
459
|
+
"model": "openrouter/z-ai/glm-5.2", "thinking": "max",
|
|
460
|
+
"fallbacks": ["openrouter/x-ai/grok-4.5", "openai/gpt-5.6-luna"]
|
|
461
|
+
},
|
|
462
|
+
"medium": {
|
|
463
|
+
"model": "openrouter/z-ai/glm-5.2", "thinking": "max",
|
|
464
|
+
"fallbacks": ["openai/gpt-5.6-luna", "openrouter/x-ai/grok-4.5"]
|
|
465
|
+
},
|
|
466
|
+
"low": {
|
|
467
|
+
"model": "openrouter/minimax/minimax-m3", "thinking": "off",
|
|
468
|
+
"fallbacks": ["openrouter/z-ai/glm-5.2"]
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
"gpt-5.6": {
|
|
472
|
+
"high": { "model": "openai/gpt-5.6-sol", "thinking": "medium" },
|
|
473
|
+
"medium": { "model": "openai/gpt-5.6-terra", "thinking": "high" },
|
|
474
|
+
"low": { "model": "openai/gpt-5.6-luna", "thinking": "high" }
|
|
475
|
+
},
|
|
476
|
+
"opus-4.8": {
|
|
477
|
+
"high": { "model": "openrouter/anthropic/claude-opus-4.8", "thinking": "medium" },
|
|
478
|
+
"medium": { "model": "openrouter/anthropic/claude-opus-4.8", "thinking": "low" },
|
|
479
|
+
"low": { "model": "openrouter/anthropic/claude-opus-4.8", "thinking": "off" }
|
|
453
480
|
}
|
|
454
481
|
}
|
|
455
482
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -62,7 +62,7 @@ Router state is persisted using `pi.appendEntry` with a custom type `router-stat
|
|
|
62
62
|
| Field | Type | Description |
|
|
63
63
|
|---|---|---|
|
|
64
64
|
| `selectedProfile` | `string` | Active profile name |
|
|
65
|
-
| `pinnedTierByProfile` | `Record<string,
|
|
65
|
+
| `pinnedTierByProfile` | `Record<string, RouterTier>` | Manual tier pins per profile |
|
|
66
66
|
| `debugEnabled` | `boolean` | Debug mode state |
|
|
67
67
|
| `lastDecision` | `RoutingDecision` | Most recent routing decision |
|
|
68
68
|
| `lastNonRouterModel` | `string` | Last model used before switching to router |
|
package/extensions/config.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
|
|
15
15
|
export const ROUTER_TIERS = ['high', 'medium', 'low'] as const;
|
|
16
16
|
|
|
17
|
-
const THINKING_LEVELS: readonly ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
17
|
+
const THINKING_LEVELS: readonly ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
18
18
|
export const ROUTER_PIN_VALUES = ['clear', 'high', 'medium', 'low'] as const;
|
|
19
19
|
|
|
20
20
|
const isObjectRecord = (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kdejaeger/pi-model-router",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Intelligent per-turn model router extension for the pi coding agent",
|
|
6
6
|
"keywords": [
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"prepublishOnly": "npm run tsc"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@earendil-works/pi-agent-core": "^0.80.
|
|
45
|
-
"@earendil-works/pi-ai": "^0.80.
|
|
46
|
-
"@earendil-works/pi-coding-agent": "^0.80.
|
|
47
|
-
"@earendil-works/pi-tui": "^0.80.
|
|
44
|
+
"@earendil-works/pi-agent-core": "^0.80.6",
|
|
45
|
+
"@earendil-works/pi-ai": "^0.80.6",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "^0.80.6",
|
|
47
|
+
"@earendil-works/pi-tui": "^0.80.6",
|
|
48
48
|
"@sinclair/typebox": "*"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|