takomi 2.1.1 → 2.1.3
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/README.md +124 -124
- package/.pi/agents/architect.md +15 -15
- package/.pi/agents/coder.md +14 -14
- package/.pi/agents/designer.md +17 -17
- package/.pi/agents/orchestrator.md +22 -22
- package/.pi/agents/reviewer.md +16 -16
- package/.pi/extensions/oauth-router/README.md +125 -125
- package/.pi/extensions/oauth-router/commands.ts +380 -380
- package/.pi/extensions/oauth-router/config.ts +200 -200
- package/.pi/extensions/oauth-router/index.ts +41 -41
- package/.pi/extensions/oauth-router/oauth-flow.ts +154 -154
- package/.pi/extensions/oauth-router/oauth-store.ts +121 -121
- package/.pi/extensions/oauth-router/package.json +14 -14
- package/.pi/extensions/oauth-router/policies.ts +27 -27
- package/.pi/extensions/oauth-router/provider.ts +492 -492
- package/.pi/extensions/oauth-router/scripts/vibe-verify.py +98 -98
- package/.pi/extensions/oauth-router/state.ts +174 -174
- package/.pi/extensions/oauth-router/types.ts +153 -153
- package/.pi/extensions/takomi-runtime/command-text.ts +130 -130
- package/.pi/extensions/takomi-runtime/commands.ts +179 -179
- package/.pi/extensions/takomi-runtime/context-panel.ts +282 -282
- package/.pi/extensions/takomi-runtime/index.ts +1288 -1288
- package/.pi/extensions/takomi-runtime/profile.ts +114 -114
- package/.pi/extensions/takomi-runtime/routing-policy.ts +105 -105
- package/.pi/extensions/takomi-runtime/shared.ts +492 -492
- package/.pi/extensions/takomi-runtime/subagent-controller.ts +364 -364
- package/.pi/extensions/takomi-runtime/subagent-render.ts +501 -501
- package/.pi/extensions/takomi-runtime/subagent-types.ts +83 -83
- package/.pi/extensions/takomi-runtime/ui.ts +133 -133
- package/.pi/extensions/takomi-subagents/agent-aliases.ts +18 -18
- package/.pi/extensions/takomi-subagents/agents.ts +113 -113
- package/.pi/extensions/takomi-subagents/delegation-plan.ts +95 -95
- package/.pi/extensions/takomi-subagents/dispatch-helpers.ts +26 -26
- package/.pi/extensions/takomi-subagents/dispatch.ts +215 -215
- package/.pi/extensions/takomi-subagents/index.ts +75 -75
- package/.pi/extensions/takomi-subagents/live-updates.ts +83 -83
- package/.pi/extensions/takomi-subagents/native-render.ts +174 -174
- package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -209
- package/.pi/themes/takomi-noir.json +81 -81
- package/package.json +59 -59
- package/src/cli.js +2 -1
- package/src/doctor.js +87 -84
- package/src/pi-harness.js +355 -351
- package/src/pi-installer.js +193 -171
- package/src/pi-takomi-core/index.ts +4 -4
- package/src/pi-takomi-core/orchestration.ts +402 -402
- package/src/pi-takomi-core/routing.ts +93 -93
- package/src/pi-takomi-core/types.ts +173 -173
- package/src/pi-takomi-core/workflows.ts +299 -299
- package/src/skills-installer.js +101 -101
|
@@ -1,380 +1,380 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { createAccountFromUpstream } from "./oauth-flow.ts";
|
|
3
|
-
import type { RouterStatusRow, RoutingPolicyName } from "./types.ts";
|
|
4
|
-
import { RouterRuntime } from "./provider.ts";
|
|
5
|
-
|
|
6
|
-
function formatWhen(timestamp?: number): string {
|
|
7
|
-
if (!timestamp) return "never";
|
|
8
|
-
return new Date(timestamp).toLocaleString();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function formatRelative(target?: number): string {
|
|
12
|
-
if (!target) return "-";
|
|
13
|
-
const delta = target - Date.now();
|
|
14
|
-
if (delta <= 0) return "expired";
|
|
15
|
-
const seconds = Math.ceil(delta / 1000);
|
|
16
|
-
if (seconds < 60) return `${seconds}s`;
|
|
17
|
-
const minutes = Math.ceil(seconds / 60);
|
|
18
|
-
if (minutes < 60) return `${minutes}m`;
|
|
19
|
-
const hours = Math.ceil(minutes / 60);
|
|
20
|
-
return `${hours}h`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function formatAgo(timestamp?: number): string {
|
|
24
|
-
if (!timestamp) return "never";
|
|
25
|
-
const delta = Date.now() - timestamp;
|
|
26
|
-
if (delta < 60_000) return `${Math.max(1, Math.ceil(delta / 1000))}s ago`;
|
|
27
|
-
if (delta < 3_600_000) return `${Math.ceil(delta / 60_000)}m ago`;
|
|
28
|
-
if (delta < 86_400_000) return `${Math.ceil(delta / 3_600_000)}h ago`;
|
|
29
|
-
return `${Math.ceil(delta / 86_400_000)}d ago`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatLastUsedSummary(timestamp?: number): string {
|
|
33
|
-
if (!timestamp) return "never";
|
|
34
|
-
return `${formatWhen(timestamp)} (${formatAgo(timestamp)})`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function isHealthy(row: RouterStatusRow): boolean {
|
|
38
|
-
if (!row.enabled) return false;
|
|
39
|
-
if (row.authHealth !== "ok") return false;
|
|
40
|
-
if (row.cooldownUntil && row.cooldownUntil > Date.now()) return false;
|
|
41
|
-
if (row.penaltyUntil && row.penaltyUntil > Date.now()) return false;
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function formatStatusReport(runtime: RouterRuntime): string {
|
|
46
|
-
const config = runtime.getConfig();
|
|
47
|
-
const rows = runtime.getStatusRows();
|
|
48
|
-
const accountsById = new Map(runtime.listAccounts().map((account) => [account.id, account]));
|
|
49
|
-
const healthyRows = rows.filter((row) => isHealthy(row));
|
|
50
|
-
const healthy = healthyRows.length;
|
|
51
|
-
const enabled = rows.filter((row) => row.enabled).length;
|
|
52
|
-
const usableModels = config.models
|
|
53
|
-
.filter((model) => {
|
|
54
|
-
const routeFilter = new Set(model.route?.upstreamIds ?? []);
|
|
55
|
-
return healthyRows.some((row) => {
|
|
56
|
-
const upstream = config.upstreams.find((entry) => entry.id === row.upstream);
|
|
57
|
-
if (!upstream?.modelIds.includes(model.id)) return false;
|
|
58
|
-
if (routeFilter.size > 0 && !routeFilter.has(row.upstream)) return false;
|
|
59
|
-
return true;
|
|
60
|
-
});
|
|
61
|
-
})
|
|
62
|
-
.map((model) => model.id);
|
|
63
|
-
const lastUsedRow = [...rows]
|
|
64
|
-
.filter((row) => row.lastUsedAt)
|
|
65
|
-
.sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0))[0];
|
|
66
|
-
const lastUsedLine = lastUsedRow
|
|
67
|
-
? `${lastUsedRow.id} | ${lastUsedRow.label} | ${lastUsedRow.upstream} | ${formatLastUsedSummary(lastUsedRow.lastUsedAt)}`
|
|
68
|
-
: "none yet";
|
|
69
|
-
const upstreams = config.upstreams.map((upstream) => {
|
|
70
|
-
const auth = upstream.authMode === "oauth" ? `oauth:${upstream.oauthProviderId}` : "api-key";
|
|
71
|
-
const activeCount = rows.filter((row) => row.upstream === upstream.id && row.enabled).length;
|
|
72
|
-
return `- ${upstream.id} | ${upstream.label} | ${auth} | ${upstream.api} | active=${activeCount} | models=${upstream.modelIds.join(", ")}`;
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const accounts = rows.length
|
|
76
|
-
? [...rows]
|
|
77
|
-
.sort((a, b) => {
|
|
78
|
-
const aTime = a.lastUsedAt ?? 0;
|
|
79
|
-
const bTime = b.lastUsedAt ?? 0;
|
|
80
|
-
if (aTime !== bTime) return bTime - aTime;
|
|
81
|
-
return a.label.localeCompare(b.label);
|
|
82
|
-
})
|
|
83
|
-
.map((row) => {
|
|
84
|
-
const account = accountsById.get(row.id);
|
|
85
|
-
const plan = typeof account?.meta?.planType === "string" ? account.meta.planType : "unknown";
|
|
86
|
-
const health = isHealthy(row)
|
|
87
|
-
? "healthy"
|
|
88
|
-
: row.authHealth !== "ok"
|
|
89
|
-
? `auth=${row.authHealth}`
|
|
90
|
-
: row.cooldownUntil && row.cooldownUntil > Date.now()
|
|
91
|
-
? `cooldown ${formatRelative(row.cooldownUntil)}`
|
|
92
|
-
: row.penaltyUntil && row.penaltyUntil > Date.now()
|
|
93
|
-
? `penalty ${formatRelative(row.penaltyUntil)}`
|
|
94
|
-
: "inactive";
|
|
95
|
-
|
|
96
|
-
return [
|
|
97
|
-
`- ${row.id} | ${row.label} | plan=${plan}`,
|
|
98
|
-
` upstream=${row.upstream} enabled=${row.enabled} weight=${row.weight} state=${health}`,
|
|
99
|
-
` lastUsed=${formatLastUsedSummary(row.lastUsedAt)} | lastStatus=${row.lastStatus ?? "-"} | expires=${formatWhen(row.expires)}`,
|
|
100
|
-
` successes=${row.successCount} failures=${row.failures} 429s=${row.rateLimitCount} authFailures=${row.authFailureCount}`,
|
|
101
|
-
].join("\n");
|
|
102
|
-
})
|
|
103
|
-
: ["- No accounts configured yet. Use /router-login add."];
|
|
104
|
-
|
|
105
|
-
return [
|
|
106
|
-
"# oauth-router status",
|
|
107
|
-
"",
|
|
108
|
-
`Policy: ${runtime.getPolicy()}`,
|
|
109
|
-
`Accounts: ${rows.length} total | ${enabled} enabled | ${healthy} healthy`,
|
|
110
|
-
`Usable models now: ${usableModels.length > 0 ? usableModels.join(", ") : "none"}`,
|
|
111
|
-
`Last used account: ${lastUsedLine}`,
|
|
112
|
-
"",
|
|
113
|
-
"## Upstreams",
|
|
114
|
-
...upstreams,
|
|
115
|
-
"",
|
|
116
|
-
"## Accounts",
|
|
117
|
-
...accounts,
|
|
118
|
-
].join("\n");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function emitReport(pi: ExtensionAPI, text: string) {
|
|
122
|
-
pi.sendMessage({
|
|
123
|
-
customType: "oauth-router",
|
|
124
|
-
content: text,
|
|
125
|
-
display: true,
|
|
126
|
-
details: { source: "oauth-router" },
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function pickUpstream(runtime: RouterRuntime, ctx: ExtensionCommandContext, requestedId?: string) {
|
|
131
|
-
const upstreams = runtime.listUpstreams().filter((upstream) => upstream.enabled);
|
|
132
|
-
if (upstreams.length === 0) throw new Error("No enabled upstreams are configured");
|
|
133
|
-
|
|
134
|
-
if (requestedId) {
|
|
135
|
-
const matched = upstreams.find((upstream) => upstream.id === requestedId);
|
|
136
|
-
if (!matched) throw new Error(`Unknown upstream: ${requestedId}`);
|
|
137
|
-
return matched;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!ctx.hasUI) return upstreams[0]!;
|
|
141
|
-
|
|
142
|
-
const choice = await ctx.ui.select(
|
|
143
|
-
"Choose an upstream",
|
|
144
|
-
upstreams.map((upstream) => `${upstream.id} — ${upstream.label}`),
|
|
145
|
-
);
|
|
146
|
-
if (!choice) throw new Error("Cancelled by user");
|
|
147
|
-
const id = choice.split(" — ")[0]?.trim();
|
|
148
|
-
const selected = upstreams.find((upstream) => upstream.id === id);
|
|
149
|
-
if (!selected) throw new Error(`Unknown upstream: ${id}`);
|
|
150
|
-
return selected;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function getLabel(ctx: ExtensionCommandContext, fallback: string, provided?: string) {
|
|
154
|
-
if (provided && provided.trim()) return provided.trim();
|
|
155
|
-
if (!ctx.hasUI) return fallback;
|
|
156
|
-
const response = await ctx.ui.input("Account label:", fallback);
|
|
157
|
-
return response?.trim() || fallback;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function normalizePolicy(input?: string): RoutingPolicyName | undefined {
|
|
161
|
-
if (!input) return undefined;
|
|
162
|
-
const value = input.trim().toLowerCase();
|
|
163
|
-
if (value === "round-robin" || value === "rr") return "round-robin";
|
|
164
|
-
if (value === "weighted-round-robin" || value === "wrr" || value === "weighted") return "weighted-round-robin";
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function parseArgs(args: string): string[] {
|
|
169
|
-
return args
|
|
170
|
-
.trim()
|
|
171
|
-
.split(/\s+/)
|
|
172
|
-
.map((value) => value.trim())
|
|
173
|
-
.filter(Boolean);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function setFooterStatus(ctx: ExtensionCommandContext, runtime: RouterRuntime) {
|
|
177
|
-
const rows = runtime.getStatusRows();
|
|
178
|
-
const healthy = rows.filter((row) => isHealthy(row)).length;
|
|
179
|
-
ctx.ui.setStatus("oauth-router", `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function getAccountUsageLines(runtime: RouterRuntime): string[] {
|
|
183
|
-
const rows = runtime.getStatusRows();
|
|
184
|
-
if (rows.length === 0) {
|
|
185
|
-
return ["No accounts configured yet. Use /router-login add."];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return rows.map((row) => `- ${row.id} | ${row.label} | enabled=${row.enabled} | weight=${row.weight}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function showCommandHint(pi: ExtensionAPI, ctx: ExtensionCommandContext, title: string, lines: string[]) {
|
|
192
|
-
emitReport(pi, [`# ${title}`, "", ...lines].join("\n"));
|
|
193
|
-
ctx.ui.notify(title, "info");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function handleRouterLogin(pi: ExtensionAPI, runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext) {
|
|
197
|
-
const [command = "help", first, ...rest] = parseArgs(args);
|
|
198
|
-
|
|
199
|
-
switch (command) {
|
|
200
|
-
case "add": {
|
|
201
|
-
const upstream = await pickUpstream(runtime, ctx, first);
|
|
202
|
-
const label = await getLabel(ctx, `${upstream.label} ${runtime.listAccounts().length + 1}`, rest.join(" "));
|
|
203
|
-
const account = await createAccountFromUpstream(upstream, label, ctx);
|
|
204
|
-
runtime.addAccount(account);
|
|
205
|
-
setFooterStatus(ctx, runtime);
|
|
206
|
-
emitReport(pi, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.`);
|
|
207
|
-
ctx.ui.notify(`Added ${account.id}`, "info");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
case "list": {
|
|
211
|
-
const report = formatStatusReport(runtime);
|
|
212
|
-
emitReport(pi, report);
|
|
213
|
-
setFooterStatus(ctx, runtime);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
case "remove": {
|
|
217
|
-
if (!first) throw new Error("Usage: /router-login remove <id>");
|
|
218
|
-
if (ctx.hasUI) {
|
|
219
|
-
const ok = await ctx.ui.confirm("Remove account?", `Delete router account ${first}?`);
|
|
220
|
-
if (!ok) throw new Error("Cancelled by user");
|
|
221
|
-
}
|
|
222
|
-
runtime.removeAccount(first);
|
|
223
|
-
setFooterStatus(ctx, runtime);
|
|
224
|
-
emitReport(pi, `Removed account ${first}.`);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
case "refresh": {
|
|
228
|
-
if (!first) throw new Error("Usage: /router-login refresh <id>");
|
|
229
|
-
const refreshed = await runtime.refreshAccount(first);
|
|
230
|
-
setFooterStatus(ctx, runtime);
|
|
231
|
-
emitReport(pi, `Refreshed account ${refreshed.id} (${refreshed.label}).`);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
case "help":
|
|
235
|
-
default: {
|
|
236
|
-
emitReport(
|
|
237
|
-
pi,
|
|
238
|
-
[
|
|
239
|
-
"# oauth-router commands",
|
|
240
|
-
"",
|
|
241
|
-
"- /router-login add [upstreamId] [label]",
|
|
242
|
-
"- /router-login list",
|
|
243
|
-
"- /router-login remove <id>",
|
|
244
|
-
"- /router-login refresh <id>",
|
|
245
|
-
"- /router-status",
|
|
246
|
-
"- /router-accounts",
|
|
247
|
-
"- /router-enable <id>",
|
|
248
|
-
"- /router-disable <id>",
|
|
249
|
-
"- /router-policy <round-robin|weighted-round-robin>",
|
|
250
|
-
"- /router-weight <id> <n>",
|
|
251
|
-
].join("\n"),
|
|
252
|
-
);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime) {
|
|
259
|
-
pi.registerCommand("router-login", {
|
|
260
|
-
description: "Manage oauth-router accounts",
|
|
261
|
-
handler: async (args, ctx) => handleRouterLogin(pi, runtime, args || "", ctx),
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
pi.registerCommand("router-status", {
|
|
265
|
-
description: "Show oauth-router health and routing state",
|
|
266
|
-
handler: async (_args, ctx) => {
|
|
267
|
-
emitReport(pi, formatStatusReport(runtime));
|
|
268
|
-
setFooterStatus(ctx, runtime);
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
pi.registerCommand("router-accounts", {
|
|
273
|
-
description: "Show oauth-router accounts",
|
|
274
|
-
handler: async (_args, ctx) => {
|
|
275
|
-
emitReport(pi, formatStatusReport(runtime));
|
|
276
|
-
setFooterStatus(ctx, runtime);
|
|
277
|
-
},
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
pi.registerCommand("router-enable", {
|
|
281
|
-
description: "Enable an oauth-router account",
|
|
282
|
-
handler: async (args, ctx) => {
|
|
283
|
-
const [id] = parseArgs(args || "");
|
|
284
|
-
if (!id) {
|
|
285
|
-
showCommandHint(pi, ctx, "router-enable", ["Usage: /router-enable <id>", "", ...getAccountUsageLines(runtime)]);
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
if (!runtime.getAccount(id)) {
|
|
289
|
-
showCommandHint(pi, ctx, "router-enable", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
runtime.setEnabled(id, true);
|
|
293
|
-
setFooterStatus(ctx, runtime);
|
|
294
|
-
emitReport(pi, `Enabled account ${id}.`);
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
pi.registerCommand("router-disable", {
|
|
299
|
-
description: "Disable an oauth-router account",
|
|
300
|
-
handler: async (args, ctx) => {
|
|
301
|
-
const [id] = parseArgs(args || "");
|
|
302
|
-
if (!id) {
|
|
303
|
-
showCommandHint(pi, ctx, "router-disable", ["Usage: /router-disable <id>", "", ...getAccountUsageLines(runtime)]);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (!runtime.getAccount(id)) {
|
|
307
|
-
showCommandHint(pi, ctx, "router-disable", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
runtime.setEnabled(id, false);
|
|
311
|
-
setFooterStatus(ctx, runtime);
|
|
312
|
-
emitReport(pi, `Disabled account ${id}.`);
|
|
313
|
-
},
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
pi.registerCommand("router-policy", {
|
|
317
|
-
description: "Set oauth-router routing policy",
|
|
318
|
-
handler: async (args, ctx) => {
|
|
319
|
-
const raw = (args || "").trim();
|
|
320
|
-
const policy = normalizePolicy(raw);
|
|
321
|
-
if (!raw) {
|
|
322
|
-
showCommandHint(pi, ctx, "router-policy", [
|
|
323
|
-
`Current policy: ${runtime.getPolicy()}`,
|
|
324
|
-
"",
|
|
325
|
-
"Usage: /router-policy <round-robin|weighted-round-robin>",
|
|
326
|
-
"Aliases: rr, wrr, weighted",
|
|
327
|
-
]);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (!policy) {
|
|
331
|
-
showCommandHint(pi, ctx, "router-policy", [
|
|
332
|
-
`Unrecognized policy: ${raw}`,
|
|
333
|
-
"",
|
|
334
|
-
`Current policy: ${runtime.getPolicy()}`,
|
|
335
|
-
"Valid values: round-robin, weighted-round-robin",
|
|
336
|
-
]);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
runtime.setPolicy(policy);
|
|
340
|
-
setFooterStatus(ctx, runtime);
|
|
341
|
-
emitReport(pi, `Routing policy set to ${policy}.`);
|
|
342
|
-
},
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
pi.registerCommand("router-weight", {
|
|
346
|
-
description: "Set oauth-router account weight for weighted round robin",
|
|
347
|
-
handler: async (args, ctx) => {
|
|
348
|
-
const [id, rawWeight] = parseArgs(args || "");
|
|
349
|
-
const hasAccounts = runtime.getStatusRows().length > 0;
|
|
350
|
-
if (!id && !rawWeight) {
|
|
351
|
-
showCommandHint(pi, ctx, "router-weight", [
|
|
352
|
-
"Usage: /router-weight <id> <n>",
|
|
353
|
-
"",
|
|
354
|
-
"Example: /router-weight acct_ab12cd34 3",
|
|
355
|
-
"",
|
|
356
|
-
...getAccountUsageLines(runtime),
|
|
357
|
-
]);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const weight = Number(rawWeight);
|
|
362
|
-
if (!id || !Number.isFinite(weight)) {
|
|
363
|
-
showCommandHint(pi, ctx, "router-weight", [
|
|
364
|
-
"Usage: /router-weight <id> <n>",
|
|
365
|
-
hasAccounts ? "Provide both an account id and a numeric weight." : "Add an account first with /router-login add.",
|
|
366
|
-
"",
|
|
367
|
-
...getAccountUsageLines(runtime),
|
|
368
|
-
]);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (!runtime.getAccount(id)) {
|
|
372
|
-
showCommandHint(pi, ctx, "router-weight", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
runtime.setWeight(id, weight);
|
|
376
|
-
setFooterStatus(ctx, runtime);
|
|
377
|
-
emitReport(pi, `Updated weight for ${id} to ${Math.max(1, Math.floor(weight))}.`);
|
|
378
|
-
},
|
|
379
|
-
});
|
|
380
|
-
}
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { createAccountFromUpstream } from "./oauth-flow.ts";
|
|
3
|
+
import type { RouterStatusRow, RoutingPolicyName } from "./types.ts";
|
|
4
|
+
import { RouterRuntime } from "./provider.ts";
|
|
5
|
+
|
|
6
|
+
function formatWhen(timestamp?: number): string {
|
|
7
|
+
if (!timestamp) return "never";
|
|
8
|
+
return new Date(timestamp).toLocaleString();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatRelative(target?: number): string {
|
|
12
|
+
if (!target) return "-";
|
|
13
|
+
const delta = target - Date.now();
|
|
14
|
+
if (delta <= 0) return "expired";
|
|
15
|
+
const seconds = Math.ceil(delta / 1000);
|
|
16
|
+
if (seconds < 60) return `${seconds}s`;
|
|
17
|
+
const minutes = Math.ceil(seconds / 60);
|
|
18
|
+
if (minutes < 60) return `${minutes}m`;
|
|
19
|
+
const hours = Math.ceil(minutes / 60);
|
|
20
|
+
return `${hours}h`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatAgo(timestamp?: number): string {
|
|
24
|
+
if (!timestamp) return "never";
|
|
25
|
+
const delta = Date.now() - timestamp;
|
|
26
|
+
if (delta < 60_000) return `${Math.max(1, Math.ceil(delta / 1000))}s ago`;
|
|
27
|
+
if (delta < 3_600_000) return `${Math.ceil(delta / 60_000)}m ago`;
|
|
28
|
+
if (delta < 86_400_000) return `${Math.ceil(delta / 3_600_000)}h ago`;
|
|
29
|
+
return `${Math.ceil(delta / 86_400_000)}d ago`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatLastUsedSummary(timestamp?: number): string {
|
|
33
|
+
if (!timestamp) return "never";
|
|
34
|
+
return `${formatWhen(timestamp)} (${formatAgo(timestamp)})`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isHealthy(row: RouterStatusRow): boolean {
|
|
38
|
+
if (!row.enabled) return false;
|
|
39
|
+
if (row.authHealth !== "ok") return false;
|
|
40
|
+
if (row.cooldownUntil && row.cooldownUntil > Date.now()) return false;
|
|
41
|
+
if (row.penaltyUntil && row.penaltyUntil > Date.now()) return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatStatusReport(runtime: RouterRuntime): string {
|
|
46
|
+
const config = runtime.getConfig();
|
|
47
|
+
const rows = runtime.getStatusRows();
|
|
48
|
+
const accountsById = new Map(runtime.listAccounts().map((account) => [account.id, account]));
|
|
49
|
+
const healthyRows = rows.filter((row) => isHealthy(row));
|
|
50
|
+
const healthy = healthyRows.length;
|
|
51
|
+
const enabled = rows.filter((row) => row.enabled).length;
|
|
52
|
+
const usableModels = config.models
|
|
53
|
+
.filter((model) => {
|
|
54
|
+
const routeFilter = new Set(model.route?.upstreamIds ?? []);
|
|
55
|
+
return healthyRows.some((row) => {
|
|
56
|
+
const upstream = config.upstreams.find((entry) => entry.id === row.upstream);
|
|
57
|
+
if (!upstream?.modelIds.includes(model.id)) return false;
|
|
58
|
+
if (routeFilter.size > 0 && !routeFilter.has(row.upstream)) return false;
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
})
|
|
62
|
+
.map((model) => model.id);
|
|
63
|
+
const lastUsedRow = [...rows]
|
|
64
|
+
.filter((row) => row.lastUsedAt)
|
|
65
|
+
.sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0))[0];
|
|
66
|
+
const lastUsedLine = lastUsedRow
|
|
67
|
+
? `${lastUsedRow.id} | ${lastUsedRow.label} | ${lastUsedRow.upstream} | ${formatLastUsedSummary(lastUsedRow.lastUsedAt)}`
|
|
68
|
+
: "none yet";
|
|
69
|
+
const upstreams = config.upstreams.map((upstream) => {
|
|
70
|
+
const auth = upstream.authMode === "oauth" ? `oauth:${upstream.oauthProviderId}` : "api-key";
|
|
71
|
+
const activeCount = rows.filter((row) => row.upstream === upstream.id && row.enabled).length;
|
|
72
|
+
return `- ${upstream.id} | ${upstream.label} | ${auth} | ${upstream.api} | active=${activeCount} | models=${upstream.modelIds.join(", ")}`;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const accounts = rows.length
|
|
76
|
+
? [...rows]
|
|
77
|
+
.sort((a, b) => {
|
|
78
|
+
const aTime = a.lastUsedAt ?? 0;
|
|
79
|
+
const bTime = b.lastUsedAt ?? 0;
|
|
80
|
+
if (aTime !== bTime) return bTime - aTime;
|
|
81
|
+
return a.label.localeCompare(b.label);
|
|
82
|
+
})
|
|
83
|
+
.map((row) => {
|
|
84
|
+
const account = accountsById.get(row.id);
|
|
85
|
+
const plan = typeof account?.meta?.planType === "string" ? account.meta.planType : "unknown";
|
|
86
|
+
const health = isHealthy(row)
|
|
87
|
+
? "healthy"
|
|
88
|
+
: row.authHealth !== "ok"
|
|
89
|
+
? `auth=${row.authHealth}`
|
|
90
|
+
: row.cooldownUntil && row.cooldownUntil > Date.now()
|
|
91
|
+
? `cooldown ${formatRelative(row.cooldownUntil)}`
|
|
92
|
+
: row.penaltyUntil && row.penaltyUntil > Date.now()
|
|
93
|
+
? `penalty ${formatRelative(row.penaltyUntil)}`
|
|
94
|
+
: "inactive";
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
`- ${row.id} | ${row.label} | plan=${plan}`,
|
|
98
|
+
` upstream=${row.upstream} enabled=${row.enabled} weight=${row.weight} state=${health}`,
|
|
99
|
+
` lastUsed=${formatLastUsedSummary(row.lastUsedAt)} | lastStatus=${row.lastStatus ?? "-"} | expires=${formatWhen(row.expires)}`,
|
|
100
|
+
` successes=${row.successCount} failures=${row.failures} 429s=${row.rateLimitCount} authFailures=${row.authFailureCount}`,
|
|
101
|
+
].join("\n");
|
|
102
|
+
})
|
|
103
|
+
: ["- No accounts configured yet. Use /router-login add."];
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
"# oauth-router status",
|
|
107
|
+
"",
|
|
108
|
+
`Policy: ${runtime.getPolicy()}`,
|
|
109
|
+
`Accounts: ${rows.length} total | ${enabled} enabled | ${healthy} healthy`,
|
|
110
|
+
`Usable models now: ${usableModels.length > 0 ? usableModels.join(", ") : "none"}`,
|
|
111
|
+
`Last used account: ${lastUsedLine}`,
|
|
112
|
+
"",
|
|
113
|
+
"## Upstreams",
|
|
114
|
+
...upstreams,
|
|
115
|
+
"",
|
|
116
|
+
"## Accounts",
|
|
117
|
+
...accounts,
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function emitReport(pi: ExtensionAPI, text: string) {
|
|
122
|
+
pi.sendMessage({
|
|
123
|
+
customType: "oauth-router",
|
|
124
|
+
content: text,
|
|
125
|
+
display: true,
|
|
126
|
+
details: { source: "oauth-router" },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function pickUpstream(runtime: RouterRuntime, ctx: ExtensionCommandContext, requestedId?: string) {
|
|
131
|
+
const upstreams = runtime.listUpstreams().filter((upstream) => upstream.enabled);
|
|
132
|
+
if (upstreams.length === 0) throw new Error("No enabled upstreams are configured");
|
|
133
|
+
|
|
134
|
+
if (requestedId) {
|
|
135
|
+
const matched = upstreams.find((upstream) => upstream.id === requestedId);
|
|
136
|
+
if (!matched) throw new Error(`Unknown upstream: ${requestedId}`);
|
|
137
|
+
return matched;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!ctx.hasUI) return upstreams[0]!;
|
|
141
|
+
|
|
142
|
+
const choice = await ctx.ui.select(
|
|
143
|
+
"Choose an upstream",
|
|
144
|
+
upstreams.map((upstream) => `${upstream.id} — ${upstream.label}`),
|
|
145
|
+
);
|
|
146
|
+
if (!choice) throw new Error("Cancelled by user");
|
|
147
|
+
const id = choice.split(" — ")[0]?.trim();
|
|
148
|
+
const selected = upstreams.find((upstream) => upstream.id === id);
|
|
149
|
+
if (!selected) throw new Error(`Unknown upstream: ${id}`);
|
|
150
|
+
return selected;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function getLabel(ctx: ExtensionCommandContext, fallback: string, provided?: string) {
|
|
154
|
+
if (provided && provided.trim()) return provided.trim();
|
|
155
|
+
if (!ctx.hasUI) return fallback;
|
|
156
|
+
const response = await ctx.ui.input("Account label:", fallback);
|
|
157
|
+
return response?.trim() || fallback;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizePolicy(input?: string): RoutingPolicyName | undefined {
|
|
161
|
+
if (!input) return undefined;
|
|
162
|
+
const value = input.trim().toLowerCase();
|
|
163
|
+
if (value === "round-robin" || value === "rr") return "round-robin";
|
|
164
|
+
if (value === "weighted-round-robin" || value === "wrr" || value === "weighted") return "weighted-round-robin";
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseArgs(args: string): string[] {
|
|
169
|
+
return args
|
|
170
|
+
.trim()
|
|
171
|
+
.split(/\s+/)
|
|
172
|
+
.map((value) => value.trim())
|
|
173
|
+
.filter(Boolean);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function setFooterStatus(ctx: ExtensionCommandContext, runtime: RouterRuntime) {
|
|
177
|
+
const rows = runtime.getStatusRows();
|
|
178
|
+
const healthy = rows.filter((row) => isHealthy(row)).length;
|
|
179
|
+
ctx.ui.setStatus("oauth-router", `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getAccountUsageLines(runtime: RouterRuntime): string[] {
|
|
183
|
+
const rows = runtime.getStatusRows();
|
|
184
|
+
if (rows.length === 0) {
|
|
185
|
+
return ["No accounts configured yet. Use /router-login add."];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return rows.map((row) => `- ${row.id} | ${row.label} | enabled=${row.enabled} | weight=${row.weight}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function showCommandHint(pi: ExtensionAPI, ctx: ExtensionCommandContext, title: string, lines: string[]) {
|
|
192
|
+
emitReport(pi, [`# ${title}`, "", ...lines].join("\n"));
|
|
193
|
+
ctx.ui.notify(title, "info");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function handleRouterLogin(pi: ExtensionAPI, runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext) {
|
|
197
|
+
const [command = "help", first, ...rest] = parseArgs(args);
|
|
198
|
+
|
|
199
|
+
switch (command) {
|
|
200
|
+
case "add": {
|
|
201
|
+
const upstream = await pickUpstream(runtime, ctx, first);
|
|
202
|
+
const label = await getLabel(ctx, `${upstream.label} ${runtime.listAccounts().length + 1}`, rest.join(" "));
|
|
203
|
+
const account = await createAccountFromUpstream(upstream, label, ctx);
|
|
204
|
+
runtime.addAccount(account);
|
|
205
|
+
setFooterStatus(ctx, runtime);
|
|
206
|
+
emitReport(pi, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.`);
|
|
207
|
+
ctx.ui.notify(`Added ${account.id}`, "info");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
case "list": {
|
|
211
|
+
const report = formatStatusReport(runtime);
|
|
212
|
+
emitReport(pi, report);
|
|
213
|
+
setFooterStatus(ctx, runtime);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
case "remove": {
|
|
217
|
+
if (!first) throw new Error("Usage: /router-login remove <id>");
|
|
218
|
+
if (ctx.hasUI) {
|
|
219
|
+
const ok = await ctx.ui.confirm("Remove account?", `Delete router account ${first}?`);
|
|
220
|
+
if (!ok) throw new Error("Cancelled by user");
|
|
221
|
+
}
|
|
222
|
+
runtime.removeAccount(first);
|
|
223
|
+
setFooterStatus(ctx, runtime);
|
|
224
|
+
emitReport(pi, `Removed account ${first}.`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
case "refresh": {
|
|
228
|
+
if (!first) throw new Error("Usage: /router-login refresh <id>");
|
|
229
|
+
const refreshed = await runtime.refreshAccount(first);
|
|
230
|
+
setFooterStatus(ctx, runtime);
|
|
231
|
+
emitReport(pi, `Refreshed account ${refreshed.id} (${refreshed.label}).`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
case "help":
|
|
235
|
+
default: {
|
|
236
|
+
emitReport(
|
|
237
|
+
pi,
|
|
238
|
+
[
|
|
239
|
+
"# oauth-router commands",
|
|
240
|
+
"",
|
|
241
|
+
"- /router-login add [upstreamId] [label]",
|
|
242
|
+
"- /router-login list",
|
|
243
|
+
"- /router-login remove <id>",
|
|
244
|
+
"- /router-login refresh <id>",
|
|
245
|
+
"- /router-status",
|
|
246
|
+
"- /router-accounts",
|
|
247
|
+
"- /router-enable <id>",
|
|
248
|
+
"- /router-disable <id>",
|
|
249
|
+
"- /router-policy <round-robin|weighted-round-robin>",
|
|
250
|
+
"- /router-weight <id> <n>",
|
|
251
|
+
].join("\n"),
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime) {
|
|
259
|
+
pi.registerCommand("router-login", {
|
|
260
|
+
description: "Manage oauth-router accounts",
|
|
261
|
+
handler: async (args, ctx) => handleRouterLogin(pi, runtime, args || "", ctx),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
pi.registerCommand("router-status", {
|
|
265
|
+
description: "Show oauth-router health and routing state",
|
|
266
|
+
handler: async (_args, ctx) => {
|
|
267
|
+
emitReport(pi, formatStatusReport(runtime));
|
|
268
|
+
setFooterStatus(ctx, runtime);
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
pi.registerCommand("router-accounts", {
|
|
273
|
+
description: "Show oauth-router accounts",
|
|
274
|
+
handler: async (_args, ctx) => {
|
|
275
|
+
emitReport(pi, formatStatusReport(runtime));
|
|
276
|
+
setFooterStatus(ctx, runtime);
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
pi.registerCommand("router-enable", {
|
|
281
|
+
description: "Enable an oauth-router account",
|
|
282
|
+
handler: async (args, ctx) => {
|
|
283
|
+
const [id] = parseArgs(args || "");
|
|
284
|
+
if (!id) {
|
|
285
|
+
showCommandHint(pi, ctx, "router-enable", ["Usage: /router-enable <id>", "", ...getAccountUsageLines(runtime)]);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (!runtime.getAccount(id)) {
|
|
289
|
+
showCommandHint(pi, ctx, "router-enable", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
runtime.setEnabled(id, true);
|
|
293
|
+
setFooterStatus(ctx, runtime);
|
|
294
|
+
emitReport(pi, `Enabled account ${id}.`);
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
pi.registerCommand("router-disable", {
|
|
299
|
+
description: "Disable an oauth-router account",
|
|
300
|
+
handler: async (args, ctx) => {
|
|
301
|
+
const [id] = parseArgs(args || "");
|
|
302
|
+
if (!id) {
|
|
303
|
+
showCommandHint(pi, ctx, "router-disable", ["Usage: /router-disable <id>", "", ...getAccountUsageLines(runtime)]);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!runtime.getAccount(id)) {
|
|
307
|
+
showCommandHint(pi, ctx, "router-disable", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
runtime.setEnabled(id, false);
|
|
311
|
+
setFooterStatus(ctx, runtime);
|
|
312
|
+
emitReport(pi, `Disabled account ${id}.`);
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
pi.registerCommand("router-policy", {
|
|
317
|
+
description: "Set oauth-router routing policy",
|
|
318
|
+
handler: async (args, ctx) => {
|
|
319
|
+
const raw = (args || "").trim();
|
|
320
|
+
const policy = normalizePolicy(raw);
|
|
321
|
+
if (!raw) {
|
|
322
|
+
showCommandHint(pi, ctx, "router-policy", [
|
|
323
|
+
`Current policy: ${runtime.getPolicy()}`,
|
|
324
|
+
"",
|
|
325
|
+
"Usage: /router-policy <round-robin|weighted-round-robin>",
|
|
326
|
+
"Aliases: rr, wrr, weighted",
|
|
327
|
+
]);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!policy) {
|
|
331
|
+
showCommandHint(pi, ctx, "router-policy", [
|
|
332
|
+
`Unrecognized policy: ${raw}`,
|
|
333
|
+
"",
|
|
334
|
+
`Current policy: ${runtime.getPolicy()}`,
|
|
335
|
+
"Valid values: round-robin, weighted-round-robin",
|
|
336
|
+
]);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
runtime.setPolicy(policy);
|
|
340
|
+
setFooterStatus(ctx, runtime);
|
|
341
|
+
emitReport(pi, `Routing policy set to ${policy}.`);
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
pi.registerCommand("router-weight", {
|
|
346
|
+
description: "Set oauth-router account weight for weighted round robin",
|
|
347
|
+
handler: async (args, ctx) => {
|
|
348
|
+
const [id, rawWeight] = parseArgs(args || "");
|
|
349
|
+
const hasAccounts = runtime.getStatusRows().length > 0;
|
|
350
|
+
if (!id && !rawWeight) {
|
|
351
|
+
showCommandHint(pi, ctx, "router-weight", [
|
|
352
|
+
"Usage: /router-weight <id> <n>",
|
|
353
|
+
"",
|
|
354
|
+
"Example: /router-weight acct_ab12cd34 3",
|
|
355
|
+
"",
|
|
356
|
+
...getAccountUsageLines(runtime),
|
|
357
|
+
]);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const weight = Number(rawWeight);
|
|
362
|
+
if (!id || !Number.isFinite(weight)) {
|
|
363
|
+
showCommandHint(pi, ctx, "router-weight", [
|
|
364
|
+
"Usage: /router-weight <id> <n>",
|
|
365
|
+
hasAccounts ? "Provide both an account id and a numeric weight." : "Add an account first with /router-login add.",
|
|
366
|
+
"",
|
|
367
|
+
...getAccountUsageLines(runtime),
|
|
368
|
+
]);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!runtime.getAccount(id)) {
|
|
372
|
+
showCommandHint(pi, ctx, "router-weight", [`Unknown account: ${id}`, "", ...getAccountUsageLines(runtime)]);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
runtime.setWeight(id, weight);
|
|
376
|
+
setFooterStatus(ctx, runtime);
|
|
377
|
+
emitReport(pi, `Updated weight for ${id} to ${Math.max(1, Math.floor(weight))}.`);
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|