pi-bedrouter 0.4.1 → 0.7.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/README.md +15 -6
- package/extensions/index.ts +38 -25
- package/package.json +2 -2
- package/src/bedrouter.ts +14 -24
- package/src/models.ts +5 -49
- package/src/selection.ts +32 -0
- package/src/settings.ts +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A [Pi](https://pi.dev) extension for [bedrouter](https://github.com/bmelton/bedr
|
|
|
4
4
|
|
|
5
5
|
- **finds bedrouter** (a configured checkout, or the npm dependency this package ships with) and installs or builds it when it is missing
|
|
6
6
|
- **starts the server** when it is not already running, seeding `.env` and `bedrouter.json` from bedrouter's examples on first run
|
|
7
|
-
- **registers a `bedrouter` provider** in Pi whose models come straight from `bedrouter.json`:
|
|
7
|
+
- **registers a `bedrouter` provider** in Pi whose models come straight from `bedrouter.json`: one `auto` model plus each enabled rung in the stack
|
|
8
8
|
- **switches the session to `bedrouter/auto`** when the server is healthy, so nobody has to pick a model
|
|
9
9
|
- **shows what served each request in the footer**, live: routed model vs requested, class and deciding signal, the classifier's note, and the session's running cost against what the requested model would have cost
|
|
10
10
|
- **totals the whole session**: every request carries Pi's session id (`x-bedrouter-session`), so bedrouter can add up the session across conversation keys, compaction and sub-agent calls; `/bedrouter usage` shows spend, what the asked-for model would have cost, and the breakdown by route
|
|
@@ -36,7 +36,7 @@ Bedrouter needs AWS credentials that can call Bedrock; see its README for the `a
|
|
|
36
36
|
| `home` | `path` if set, else `~/.bedrouter` | Working directory for the server: `.env`, `bedrouter.json`, `bedrouter.log.jsonl`, `server.log` |
|
|
37
37
|
| `port` | `20129` | Port to run / expect the server on (`BEDROUTER_PORT` overrides) |
|
|
38
38
|
| `autoStart` | `true` | Start the server on session start when it is not running |
|
|
39
|
-
| `autoSelect` | `"auto"` | Model id to
|
|
39
|
+
| `autoSelect` | `"auto"` | Model id to select when no higher-precedence choice exists (`"auto"`, a rung alias, or `false` to leave the model alone) |
|
|
40
40
|
| `debug` | `false` | Start the server with `BEDROUTER_DEBUG=1` (per-request trace in `server.log`) |
|
|
41
41
|
| `footer` | `true` | Show the routing status line in Pi's footer |
|
|
42
42
|
| `providerName` | `"bedrouter"` | Provider name registered in Pi |
|
|
@@ -46,9 +46,18 @@ Bedrouter needs AWS credentials that can call Bedrock; see its README for the `a
|
|
|
46
46
|
Example for a developer with a checkout:
|
|
47
47
|
|
|
48
48
|
```json
|
|
49
|
-
{ "path": "~/projects/ai/bedrouter", "autoSelect": "auto
|
|
49
|
+
{ "path": "~/projects/ai/bedrouter", "autoSelect": "auto" }
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
Automatic selection applies only when the session did not name a provider or
|
|
53
|
+
model and Pi's `defaultProvider` is absent or `bedrouter`. An explicit
|
|
54
|
+
`--provider`/`--model` choice or a different `defaultProvider` leaves the session
|
|
55
|
+
on that provider and does not start the bedrouter server. Set
|
|
56
|
+
`PI_BEDROUTER_AUTOSELECT=0` to disable automatic startup and selection for one
|
|
57
|
+
process. The provider remains registered, so `/model`, `--list-models`, and a
|
|
58
|
+
later switch to `bedrouter/*` still work; switching later starts the server when
|
|
59
|
+
`autoStart` is enabled.
|
|
60
|
+
|
|
52
61
|
## Commands
|
|
53
62
|
|
|
54
63
|
| Command | Does |
|
|
@@ -56,12 +65,12 @@ Example for a developer with a checkout:
|
|
|
56
65
|
| `/bedrouter` or `/bedrouter status` | Install location, server health (pid, version, region, classifier), registered models, current model, last decision |
|
|
57
66
|
| `/bedrouter start` / `stop` / `restart` | Manage the server. It is shared by every Pi session, so `stop` affects all of them; see `stopOnExit` for what happens when Pi quits |
|
|
58
67
|
| `/bedrouter install` | `npm install` the dependency, or `npm run build` a checkout that has no `dist/` |
|
|
59
|
-
| `/bedrouter doctor` | Which credential source resolved, expiry, the loaded
|
|
68
|
+
| `/bedrouter doctor` | Which credential source resolved, expiry, and the loaded stack |
|
|
60
69
|
| `/bedrouter probe` | One 1-token request per rung: which models this AWS account can actually invoke |
|
|
61
70
|
| `/bedrouter usage` | This Pi session so far: requests, tokens, spend (models + classifier), what the asked-for model would have cost, saved/over, and a per-route table. Whole-session totals need bedrouter ≥ 0.3; against an older server it shows the current conversation and says so. `usage all` lists recent sessions on the server (`*` marks this one) |
|
|
62
71
|
| `/bedrouter report [--since t] [--session key] [--json]` | Savings report over the decision log (all clients, all sessions; `--session` narrows it to one) |
|
|
63
72
|
| `/bedrouter log [n]` | The last n routing decisions, one line each |
|
|
64
|
-
| `/bedrouter models` | Re-read `bedrouter.json` and re-register the provider (after editing the
|
|
73
|
+
| `/bedrouter models` | Re-read `bedrouter.json` and re-register the provider (after editing the stack) |
|
|
65
74
|
| `/bedrouter fitnotes` | Merge model notes into `~/.pi/agent/workflows.json` so the pi-agents planner defaults to `bedrouter/auto` and only pins premium rungs for planning/review |
|
|
66
75
|
| `/bedrouter config` | Show (and create) the settings file |
|
|
67
76
|
|
|
@@ -73,7 +82,7 @@ Under a coding agent every request carries tools and a large system prompt, so y
|
|
|
73
82
|
|
|
74
83
|
## Model ids
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
`auto` hands the whole choice to bedrouter's ordered, capability-filtered stack. Every enabled rung is also registered for explicit pinning. Pi reads each model's API, input modes, context window, output cap, and price from the capability block returned by `/v1/models`; bedrouter's log and `/bedrouter report` record what was actually charged.
|
|
77
86
|
|
|
78
87
|
If your `~/.pi/agent/settings.json` has an `enabledModels` allowlist, add `bedrouter/auto` (and any rungs you want visible) or the provider's models will be hidden.
|
|
79
88
|
|
package/extensions/index.ts
CHANGED
|
@@ -15,9 +15,10 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
15
15
|
import { loadSettings, saveSettings, settingsPath, agentDir, type Settings } from "../src/settings.js";
|
|
16
16
|
import * as br from "../src/bedrouter.js";
|
|
17
17
|
import { fitNotes, piModels } from "../src/models.js";
|
|
18
|
+
import { wantsUs } from "../src/selection.js";
|
|
18
19
|
import { downLine, fromHeaders, readyLine, restartingLine, sessionsTable, statusLine, usageReport, type CostTotals, type LastDecision, type Paint } from "../src/footer.js";
|
|
19
20
|
|
|
20
|
-
export
|
|
21
|
+
export async function activate(pi: ExtensionAPI, runtime: typeof br = br) {
|
|
21
22
|
let settings = loadSettings();
|
|
22
23
|
let last: LastDecision | null = null;
|
|
23
24
|
let stats: CostTotals | null = null; // footer totals: the session when the server supports it, else the last conversation
|
|
@@ -33,8 +34,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
33
34
|
/** Whole-session totals from bedrouter (>= 0.3); falls back to the last conversation on older servers. */
|
|
34
35
|
async function refreshStats(ctx: ExtensionContext): Promise<{ session: br.SessionStats | null; conversation: br.ConversationStats | null }> {
|
|
35
36
|
const id = sessionId(ctx);
|
|
36
|
-
const session = id ? await
|
|
37
|
-
const conversation = !session && last?.conversation ? await
|
|
37
|
+
const session = id ? await runtime.session(settings, id) : null;
|
|
38
|
+
const conversation = !session && last?.conversation ? await runtime.conversation(settings, last.conversation) : null;
|
|
38
39
|
stats = session ?? conversation ?? stats;
|
|
39
40
|
return { session, conversation };
|
|
40
41
|
}
|
|
@@ -47,24 +48,24 @@ export default async function (pi: ExtensionAPI) {
|
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Register (or re-register) the provider. Source, in order: the running server's /v1/models (always right for the
|
|
50
|
-
*
|
|
51
|
+
* stack that is actually serving), then bedrouter.json (home dir, then the checkout), then the shipped default.
|
|
51
52
|
*/
|
|
52
53
|
async function registerProvider(loc: br.Found | null): Promise<string> {
|
|
53
54
|
let cfg: br.BedrouterConfig | null = null;
|
|
54
55
|
let source = "";
|
|
55
|
-
const live = await
|
|
56
|
-
if (live
|
|
56
|
+
const live = await runtime.liveConfig(settings);
|
|
57
|
+
if (live?.stack.length) { cfg = live; source = `${br.baseUrl(settings)}/v1/models`; }
|
|
57
58
|
if (!cfg) {
|
|
58
|
-
const file =
|
|
59
|
+
const file = runtime.readConfig(settings, loc);
|
|
59
60
|
if ("config" in file) { cfg = file.config; source = file.path; }
|
|
60
61
|
}
|
|
61
|
-
if (!cfg) { cfg =
|
|
62
|
+
if (!cfg) { cfg = runtime.FALLBACK_CONFIG; source = "built-in default stack (no server, no bedrouter.json yet)"; }
|
|
62
63
|
const models = piModels(cfg, br.baseUrl(settings));
|
|
63
64
|
pi.registerProvider(settings.providerName, {
|
|
64
65
|
name: "bedrouter (Bedrock, routed)",
|
|
65
66
|
baseUrl: br.baseUrl(settings),
|
|
66
67
|
apiKey: process.env.BEDROUTER_API_KEY || "bedrouter",
|
|
67
|
-
api: "
|
|
68
|
+
api: "openai-completions",
|
|
68
69
|
models,
|
|
69
70
|
});
|
|
70
71
|
registeredModelIds = models.map((m) => m.id);
|
|
@@ -76,25 +77,25 @@ export default async function (pi: ExtensionAPI) {
|
|
|
76
77
|
/** Locate → (install) → start → register. Returns a human summary. Never throws. */
|
|
77
78
|
async function bringUp(ctx: ExtensionContext | null, opts: { install?: boolean; start?: boolean } = {}): Promise<{ ok: boolean; lines: string[] }> {
|
|
78
79
|
const lines: string[] = [];
|
|
79
|
-
let loc =
|
|
80
|
+
let loc = runtime.locate(settings);
|
|
80
81
|
if (!loc.found && opts.install) {
|
|
81
82
|
lines.push(`bedrouter not found (${loc.reason}); installing…`);
|
|
82
|
-
const r =
|
|
83
|
+
const r = runtime.install(settings);
|
|
83
84
|
lines.push(r.log.trim().split("\n").slice(-6).join("\n"));
|
|
84
|
-
loc =
|
|
85
|
+
loc = runtime.locate(settings);
|
|
85
86
|
}
|
|
86
87
|
if (!loc.found) { lines.push(`bedrouter is not installed: ${loc.reason}. Run /bedrouter install (or: ${loc.installCmd}).`); return { ok: false, lines }; }
|
|
87
88
|
if (!loc.cli && opts.install) {
|
|
88
89
|
lines.push(`bedrouter at ${loc.dir} is not built; building…`);
|
|
89
|
-
const r =
|
|
90
|
+
const r = runtime.install(settings);
|
|
90
91
|
lines.push(r.log.trim().split("\n").slice(-6).join("\n"));
|
|
91
|
-
loc =
|
|
92
|
+
loc = runtime.locate(settings);
|
|
92
93
|
}
|
|
93
94
|
lines.push(await registerProvider(loc as br.Found));
|
|
94
95
|
if (opts.start) {
|
|
95
|
-
const before = await
|
|
96
|
-
const r = await
|
|
97
|
-
for (const f of r.created) lines.push(`created ${f} from the example; edit it for this machine (AWS_PROFILE,
|
|
96
|
+
const before = await runtime.health(settings);
|
|
97
|
+
const r = await runtime.start(settings, loc as br.Found);
|
|
98
|
+
for (const f of r.created) lines.push(`created ${f} from the example; edit it for this machine (AWS_PROFILE, stack)`);
|
|
98
99
|
if (r.ok) {
|
|
99
100
|
if (!before?.ok) startedHere = true;
|
|
100
101
|
lines.push(`bedrouter ${r.health.version} up on ${br.baseUrl(settings)} (pid ${r.health.pid}, region ${r.health.region}, classifier ${r.health.classifier ?? "off"})${!before?.ok ? `; started by this session, on quit: ${exitPolicyText()}` : "; was already running (not started here, left alone on quit)"}`);
|
|
@@ -106,6 +107,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
106
107
|
async function autoSelect(ctx: ExtensionContext) {
|
|
107
108
|
if (settings.autoSelect === false || !ctx.hasUI) return;
|
|
108
109
|
if (isOurs(ctx)) return;
|
|
110
|
+
if (!wantsUs(settings)) return;
|
|
109
111
|
const want = registeredModelIds.includes(settings.autoSelect) ? settings.autoSelect : registeredModelIds.find((id) => id.startsWith("auto")) ?? registeredModelIds[0];
|
|
110
112
|
if (!want) return;
|
|
111
113
|
const model = ctx.modelRegistry.find(settings.providerName, want);
|
|
@@ -118,7 +120,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
118
120
|
// The factory is async, so pi waits for this: the provider exists for `pi --list-models` and `--provider bedrouter`
|
|
119
121
|
// whether or not the server or a checkout is present yet.
|
|
120
122
|
{
|
|
121
|
-
const loc =
|
|
123
|
+
const loc = runtime.locate(settings);
|
|
122
124
|
await registerProvider(loc.found ? loc : null);
|
|
123
125
|
}
|
|
124
126
|
|
|
@@ -126,22 +128,31 @@ export default async function (pi: ExtensionAPI) {
|
|
|
126
128
|
lastCtx = ctx;
|
|
127
129
|
if (ev.reason !== "startup" && ev.reason !== "new") { if (isOurs(ctx)) setStatus(ctx, statusLine(last, stats, paint(ctx))); return; }
|
|
128
130
|
last = null; stats = null;
|
|
129
|
-
const h = await
|
|
131
|
+
const h = await runtime.health(settings);
|
|
130
132
|
serverUp = !!h?.ok;
|
|
131
|
-
|
|
133
|
+
const intended = wantsUs(settings);
|
|
134
|
+
if (!h?.ok && settings.autoStart && intended) {
|
|
132
135
|
const r = await bringUp(ctx, { install: true, start: true });
|
|
133
136
|
serverUp = r.ok;
|
|
134
137
|
if (!r.ok) { notify(ctx, `bedrouter: ${r.lines[r.lines.length - 1]}`, "warning"); setStatus(ctx, downLine("/bedrouter start", paint(ctx))); return; }
|
|
135
138
|
const created = r.lines.filter((l) => l.startsWith("created "));
|
|
136
139
|
if (created.length) notify(ctx, created.join("\n"), "warning");
|
|
137
|
-
} else if (!h?.ok) { setStatus(ctx, downLine("/bedrouter start", paint(ctx))); return; }
|
|
140
|
+
} else if (!h?.ok && intended) { setStatus(ctx, downLine("/bedrouter start", paint(ctx))); return; }
|
|
141
|
+
else if (!intended) { setStatus(ctx, undefined); return; }
|
|
138
142
|
await autoSelect(ctx);
|
|
139
143
|
if (isOurs(ctx)) setStatus(ctx, readyText(ctx));
|
|
140
144
|
});
|
|
141
145
|
|
|
142
146
|
pi.on("model_select", async (ev, ctx) => {
|
|
143
|
-
if (ev.model.provider
|
|
144
|
-
|
|
147
|
+
if (ev.model.provider !== settings.providerName) { setStatus(ctx, undefined); return; }
|
|
148
|
+
const h = await runtime.health(settings);
|
|
149
|
+
serverUp = !!h?.ok;
|
|
150
|
+
if (!h?.ok && settings.autoStart) {
|
|
151
|
+
const r = await bringUp(ctx, { install: true, start: true });
|
|
152
|
+
serverUp = r.ok;
|
|
153
|
+
if (!r.ok) { notify(ctx, `bedrouter: ${r.lines[r.lines.length - 1]}`, "warning"); setStatus(ctx, downLine("/bedrouter start", paint(ctx))); return; }
|
|
154
|
+
}
|
|
155
|
+
setStatus(ctx, serverUp ? statusLine(last, stats, paint(ctx)) : downLine("/bedrouter start", paint(ctx)));
|
|
145
156
|
});
|
|
146
157
|
|
|
147
158
|
// ---- live routing display ------------------------------------------------------------------------------------
|
|
@@ -176,7 +187,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
176
187
|
async function checkHealth() {
|
|
177
188
|
const ctx = lastCtx;
|
|
178
189
|
if (!ctx || !isOurs(ctx)) return;
|
|
179
|
-
const h = await
|
|
190
|
+
const h = await runtime.health(settings);
|
|
180
191
|
const up = !!h?.ok;
|
|
181
192
|
if (up === serverUp) return;
|
|
182
193
|
serverUp = up;
|
|
@@ -301,8 +312,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
301
312
|
break;
|
|
302
313
|
}
|
|
303
314
|
default:
|
|
304
|
-
show("bedrouter", `/bedrouter ${SUB.join(" | ")}\n\nstatus install, server, provider, current model, last decision\nstart start the server if needed (seeds .env / bedrouter.json on first run)\nstop stop the server (shared by all Pi sessions)\ninstall npm install / build the bedrouter dependency\ndoctor credential source + loaded
|
|
315
|
+
show("bedrouter", `/bedrouter ${SUB.join(" | ")}\n\nstatus install, server, provider, current model, last decision\nstart start the server if needed (seeds .env / bedrouter.json on first run)\nstop stop the server (shared by all Pi sessions)\ninstall npm install / build the bedrouter dependency\ndoctor credential source + loaded stack; probe: 1-token call per rung\nreport savings report over the decision log (args pass through: --since, --json)\nlog [n] last n routing decisions\nmodels re-read bedrouter.json and re-register the provider\nfitnotes write pi-agents model notes so the planner defaults to the router\nconfig show/create ${settingsPath()}`);
|
|
305
316
|
}
|
|
306
317
|
},
|
|
307
318
|
});
|
|
308
319
|
}
|
|
320
|
+
|
|
321
|
+
export default activate;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bedrouter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Pi extension for bedrouter: starts the cost-aware Bedrock model router, registers it as a provider, and shows which model served each request",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"prepublishOnly": "npm test && npm run typecheck"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"bedrouter": "^0.
|
|
46
|
+
"bedrouter": "^0.9.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@earendil-works/pi-coding-agent": ">=0.85.0"
|
package/src/bedrouter.ts
CHANGED
|
@@ -10,8 +10,9 @@ export type Health = { ok: boolean; region: string; pid: number; version: string
|
|
|
10
10
|
export type ConversationStats = { key: string; requests: number; costUsd: number; requestedCostUsd: number; classifierCostUsd: number; inputTokens: number; outputTokens: number; escalations: number; class: string | null; routedModel: string | null; requestedModel: string | null; lastTs: string };
|
|
11
11
|
/** Per-session totals from GET /v1/sessions/:key (bedrouter >= 0.3): every request this Pi session sent, across conversations. */
|
|
12
12
|
export type SessionStats = { key: string; requests: number; errors: number; conversations: number; costUsd: number; requestedCostUsd: number; classifierCostUsd: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; escalations: number; byRoute: Record<string, { requests: number; costUsd: number; requestedCostUsd: number; inputTokens: number; outputTokens: number }>; firstTs: string; lastTs: string };
|
|
13
|
-
export type
|
|
14
|
-
export type
|
|
13
|
+
export type Capabilities = { transport: "bedrock-runtime"; api: "converse"; toolUse: boolean; streaming: boolean; imageInput: boolean; structuredOutputs: boolean; promptCaching: boolean; contextWindow: number; maxOutput: number };
|
|
14
|
+
export type Rung = { alias: string; bedrockId: string; vendor: string; enabled: boolean; inputPerM: number; outputPerM: number; serves: ("trivial" | "execute" | "explore")[]; capabilities: Capabilities };
|
|
15
|
+
export type BedrouterConfig = { stack: Rung[]; aliases?: Record<string, string>; routing?: { enabled?: boolean; classifier?: { enabled?: boolean; model?: string } } };
|
|
15
16
|
|
|
16
17
|
export type Found = { found: true; dir: string; cli: string | null; source: "settings.path" | "dependency"; version: string };
|
|
17
18
|
export type Install = Found | { found: false; reason: string; installCmd: string };
|
|
@@ -91,39 +92,28 @@ export const health = (s: Settings) => getJson<Health>(`${baseUrl(s)}/health`);
|
|
|
91
92
|
|
|
92
93
|
/** Reconstruct a BedrouterConfig from a running server's /v1/models, so the provider can be registered with no local files. */
|
|
93
94
|
export async function liveConfig(s: Settings): Promise<BedrouterConfig | null> {
|
|
94
|
-
type M = { id: string; bedrock_id: string; bedrouter?:
|
|
95
|
+
type M = { id: string; bedrock_id: string; bedrouter?: Omit<Rung, "alias" | "bedrockId"> & { rung: string; auto: boolean } };
|
|
95
96
|
const r = await getJson<{ data: M[] }>(`${baseUrl(s)}/v1/models`);
|
|
96
97
|
if (!r?.data) return null;
|
|
97
|
-
const
|
|
98
|
+
const stack: Rung[] = [];
|
|
98
99
|
const aliases: Record<string, string> = {};
|
|
99
100
|
for (const m of r.data) {
|
|
100
101
|
const b = m.bedrouter;
|
|
101
102
|
if (!b) continue;
|
|
102
|
-
if (b.auto)
|
|
103
|
-
|
|
104
|
-
if (m.id === b.rung) { if (!fam.some((x) => x.alias === b.rung)) fam.push({ alias: b.rung, bedrockId: m.bedrock_id, inputPerM: b.inputPerM, outputPerM: b.outputPerM }); }
|
|
103
|
+
if (b.auto) continue;
|
|
104
|
+
if (m.id === b.rung && !stack.some((x) => x.alias === b.rung)) stack.push({ alias: b.rung, bedrockId: m.bedrock_id, vendor: b.vendor, enabled: b.enabled, serves: b.serves, capabilities: b.capabilities, inputPerM: b.inputPerM, outputPerM: b.outputPerM });
|
|
105
105
|
else aliases[m.id] = b.rung;
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
for (const fam of Object.values(families)) fam.sort((a, b) => a.inputPerM - b.inputPerM);
|
|
109
|
-
return { families, aliases };
|
|
107
|
+
return { stack, aliases };
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
/** Last resort when neither a server nor a config file is available
|
|
110
|
+
/** Last resort when neither a server nor a config file is available. */
|
|
113
111
|
export const FALLBACK_CONFIG: BedrouterConfig = {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
],
|
|
120
|
-
openai: [
|
|
121
|
-
{ alias: "gpt-oss-20b", bedrockId: "openai.gpt-oss-20b-1:0", inputPerM: 0.07, outputPerM: 0.2 },
|
|
122
|
-
{ alias: "gpt-oss-120b", bedrockId: "openai.gpt-oss-120b-1:0", inputPerM: 0.15, outputPerM: 0.6 },
|
|
123
|
-
],
|
|
124
|
-
},
|
|
125
|
-
aliases: { auto: "auto:anthropic", "auto-oss": "auto:openai" },
|
|
126
|
-
routing: { classes: { anthropic: { trivial: "haiku", execute: "sonnet", explore: "opus" }, openai: { execute: "gpt-oss-20b", explore: "gpt-oss-120b" } } },
|
|
112
|
+
stack: [
|
|
113
|
+
{ alias:"nova-micro",bedrockId:"us.amazon.nova-micro-v1:0",vendor:"amazon",enabled:true,inputPerM:.035,outputPerM:.14,serves:["trivial"],capabilities:{transport:"bedrock-runtime",api:"converse",toolUse:true,streaming:true,imageInput:false,structuredOutputs:false,promptCaching:true,contextWindow:128000,maxOutput:5000}},
|
|
114
|
+
{ alias:"gpt-oss-120b",bedrockId:"openai.gpt-oss-120b-1:0",vendor:"openai",enabled:true,inputPerM:.15,outputPerM:.6,serves:["execute"],capabilities:{transport:"bedrock-runtime",api:"converse",toolUse:true,streaming:true,imageInput:false,structuredOutputs:true,promptCaching:false,contextWindow:128000,maxOutput:16000}},
|
|
115
|
+
{ alias:"glm-5",bedrockId:"zai.glm-5",vendor:"zai",enabled:true,inputPerM:1,outputPerM:3.2,serves:["execute","explore"],capabilities:{transport:"bedrock-runtime",api:"converse",toolUse:false,streaming:true,imageInput:false,structuredOutputs:true,promptCaching:false,contextWindow:200000,maxOutput:128000}}
|
|
116
|
+
]
|
|
127
117
|
};
|
|
128
118
|
export const conversation = (s: Settings, key: string) => getJson<ConversationStats>(`${baseUrl(s)}/v1/conversations/${key}`);
|
|
129
119
|
export const session = (s: Settings, key: string) => getJson<SessionStats>(`${baseUrl(s)}/v1/sessions/${encodeURIComponent(key)}`);
|
package/src/models.ts
CHANGED
|
@@ -1,50 +1,6 @@
|
|
|
1
|
-
// Turn bedrouter's config into Pi provider model definitions.
|
|
2
1
|
import type { BedrouterConfig, Rung } from "./bedrouter.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const FAMILY: Record<string, { api: PiModel["api"]; path: string; input: PiModel["input"]; contextWindow: number; maxTokens: number }> = {
|
|
10
|
-
anthropic: { api: "anthropic-messages", path: "", input: ["text", "image"], contextWindow: 200_000, maxTokens: 64_000 },
|
|
11
|
-
openai: { api: "openai-completions", path: "/v1", input: ["text"], contextWindow: 128_000, maxTokens: 32_000 },
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const cost = (r: Rung) => ({ input: r.inputPerM, output: r.outputPerM, cacheRead: +(r.inputPerM * 0.1).toFixed(4), cacheWrite: +(r.inputPerM * 1.25).toFixed(4) });
|
|
15
|
-
|
|
16
|
-
/** `auto` aliases first (what most people should pick), then each family's rungs. Client aliases (claude-sonnet-5 → sonnet) are omitted to keep /model short. */
|
|
17
|
-
export function piModels(cfg: BedrouterConfig, base: string): PiModel[] {
|
|
18
|
-
const out: PiModel[] = [];
|
|
19
|
-
for (const [alias, target] of Object.entries(cfg.aliases ?? {})) {
|
|
20
|
-
const m = /^auto:(\w+)$/.exec(target);
|
|
21
|
-
if (!m) continue;
|
|
22
|
-
const family = m[1], rungs = cfg.families[family], f = FAMILY[family];
|
|
23
|
-
if (!rungs?.length || !f) continue;
|
|
24
|
-
const exec = cfg.routing?.classes?.[family]?.execute;
|
|
25
|
-
const rep = rungs.find((r) => r.alias === exec) ?? rungs[0];
|
|
26
|
-
out.push({ id: alias, name: `Auto · ${family} ladder (${rungs.map((r) => r.alias).join(" → ")})`, api: f.api, baseUrl: base + f.path, reasoning: true, input: f.input, cost: cost(rep), contextWindow: f.contextWindow, maxTokens: f.maxTokens });
|
|
27
|
-
}
|
|
28
|
-
for (const [family, rungs] of Object.entries(cfg.families)) {
|
|
29
|
-
const f = FAMILY[family];
|
|
30
|
-
if (!f) continue;
|
|
31
|
-
for (const r of rungs) out.push({ id: r.alias, name: `${r.alias} · ${family} (${r.bedrockId})`, api: f.api, baseUrl: base + f.path, reasoning: true, input: f.input, cost: cost(r), contextWindow: f.contextWindow, maxTokens: f.maxTokens });
|
|
32
|
-
}
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** pi-agents "fit notes" (~/.pi/agent/workflows.json → models) so the planner prefers the router and never pins premium rungs by default. */
|
|
37
|
-
export function fitNotes(cfg: BedrouterConfig, provider: string): Record<string, string> {
|
|
38
|
-
const notes: Record<string, string> = {};
|
|
39
|
-
for (const [alias, target] of Object.entries(cfg.aliases ?? {})) {
|
|
40
|
-
if (/^auto:/.test(target)) notes[`${provider}/${alias}`] = "default for every node: bedrouter picks the cheapest adequate model per request and escalates on failure";
|
|
41
|
-
}
|
|
42
|
-
for (const [family, rungs] of Object.entries(cfg.families)) {
|
|
43
|
-
const classes = cfg.routing?.classes?.[family] ?? {};
|
|
44
|
-
for (const r of rungs) {
|
|
45
|
-
const cls = Object.entries(classes).find(([, a]) => a === r.alias)?.[0];
|
|
46
|
-
notes[`${provider}/${r.alias}`] = cls === "explore" ? "pin only for planning, final review, reduces" : cls === "trivial" ? "pin only for titles, summaries, extraction" : cls === "execute" ? "pin only when a node must not be routed; ordinary implementation" : "pinned rung, bypasses routing";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return notes;
|
|
50
|
-
}
|
|
2
|
+
export type PiModel={id:string;name:string;api:"openai-completions";baseUrl:string;reasoning:boolean;input:("text"|"image")[];cost:{input:number;output:number;cacheRead:number;cacheWrite:number};contextWindow:number;maxTokens:number};
|
|
3
|
+
const cost=(r:Rung)=>({input:r.inputPerM,output:r.outputPerM,cacheRead:+(r.inputPerM*.1).toFixed(4),cacheWrite:+(r.inputPerM*1.25).toFixed(4)});
|
|
4
|
+
const model=(id:string,r:Rung,base:string,name:string):PiModel=>({id,name,api:"openai-completions",baseUrl:base+"/v1",reasoning:true,input:r.capabilities.imageInput?["text","image"]:["text"],cost:cost(r),contextWindow:r.capabilities.contextWindow,maxTokens:r.capabilities.maxOutput});
|
|
5
|
+
export function piModels(cfg:BedrouterConfig,base:string):PiModel[]{const enabled=cfg.stack.filter(r=>r.enabled),rep=enabled.find(r=>r.serves.includes("execute"))??enabled[0],out:PiModel[]=[];if(rep){const auto={...rep,capabilities:{...rep.capabilities,imageInput:enabled.some(r=>r.capabilities.imageInput),contextWindow:Math.max(...enabled.map(r=>r.capabilities.contextWindow)),maxOutput:Math.max(...enabled.map(r=>r.capabilities.maxOutput))}};out.push(model("auto",auto,base,`Auto · stack (${enabled.map(r=>r.alias).join(" → ")})`))}for(const r of enabled)out.push(model(r.alias,r,base,`${r.alias} · ${r.vendor} (${r.bedrockId})`));return out}
|
|
6
|
+
export function fitNotes(cfg:BedrouterConfig,provider:string):Record<string,string>{const notes:Record<string,string>={[`${provider}/auto`]:"default for every node: bedrouter picks the first eligible model serving the request class and escalates on failure"};for(const r of cfg.stack.filter(r=>r.enabled)){const s=r.serves;notes[`${provider}/${r.alias}`]=s.includes("explore")?"pin only for planning or final review; ordinary work should use bedrouter/auto":s.includes("execute")?"pin only when a node must bypass routing; ordinary implementation should use bedrouter/auto":"pin only for titles, summaries, or extraction"}return notes}
|
package/src/selection.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { agentDir, type Settings } from "./settings.js";
|
|
4
|
+
|
|
5
|
+
/** True when this Pi process was launched with an explicit provider or model choice. */
|
|
6
|
+
export function launchedWithExplicitModel(argv: string[] = process.argv.slice(2)): boolean {
|
|
7
|
+
for (const arg of argv) {
|
|
8
|
+
if (arg === "--") break;
|
|
9
|
+
if (arg === "--provider" || arg === "--model") return true;
|
|
10
|
+
if (arg.startsWith("--provider=") || arg.startsWith("--model=")) return true;
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function defaultProvider(): string | undefined {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(fs.readFileSync(path.join(agentDir(), "settings.json"), "utf8"));
|
|
18
|
+
return typeof value.defaultProvider === "string" ? value.defaultProvider : undefined;
|
|
19
|
+
} catch { return undefined; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Whether automatic startup/selection should claim this session for bedrouter. */
|
|
23
|
+
export function wantsUs(
|
|
24
|
+
settings: Pick<Settings, "providerName">,
|
|
25
|
+
options: { argv?: string[]; env?: NodeJS.ProcessEnv; defaultProvider?: () => string | undefined } = {},
|
|
26
|
+
): boolean {
|
|
27
|
+
const env = options.env ?? process.env;
|
|
28
|
+
if (env.PI_BEDROUTER_AUTOSELECT === "0") return false;
|
|
29
|
+
if (launchedWithExplicitModel(options.argv ?? process.argv.slice(2))) return false;
|
|
30
|
+
const provider = (options.defaultProvider ?? defaultProvider)();
|
|
31
|
+
return !provider || provider === settings.providerName;
|
|
32
|
+
}
|
package/src/settings.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type Settings = {
|
|
|
12
12
|
port: number;
|
|
13
13
|
/** Start the server on session start when it is not running. */
|
|
14
14
|
autoStart: boolean;
|
|
15
|
-
/** Model id (from bedrouter's aliases
|
|
15
|
+
/** Model id (from bedrouter's aliases) to select only when no higher-precedence provider/model choice exists; false leaves the model alone. */
|
|
16
16
|
autoSelect: string | false;
|
|
17
17
|
/** Start the server with BEDROUTER_DEBUG=1 (per-request trace in server.log). */
|
|
18
18
|
debug: boolean;
|