takomi 2.0.7 → 2.1.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/.pi/README.md +124 -0
- package/.pi/agents/architect.md +16 -0
- package/.pi/agents/coder.md +15 -0
- package/.pi/agents/designer.md +18 -0
- package/.pi/agents/orchestrator.md +23 -0
- package/.pi/agents/reviewer.md +17 -0
- package/.pi/extensions/oauth-router/README.md +125 -0
- package/.pi/extensions/oauth-router/commands.ts +380 -0
- package/.pi/extensions/oauth-router/config.ts +200 -0
- package/.pi/extensions/oauth-router/index.ts +41 -0
- package/.pi/extensions/oauth-router/oauth-flow.ts +154 -0
- package/.pi/extensions/oauth-router/oauth-store.ts +121 -0
- package/.pi/extensions/oauth-router/package.json +14 -0
- package/.pi/extensions/oauth-router/policies.ts +27 -0
- package/.pi/extensions/oauth-router/provider.ts +492 -0
- package/.pi/extensions/oauth-router/scripts/vibe-verify.py +98 -0
- package/.pi/extensions/oauth-router/state.ts +174 -0
- package/.pi/extensions/oauth-router/types.ts +153 -0
- package/.pi/extensions/takomi-runtime/command-text.ts +130 -0
- package/.pi/extensions/takomi-runtime/commands.ts +179 -0
- package/.pi/extensions/takomi-runtime/context-panel.ts +282 -0
- package/.pi/extensions/takomi-runtime/index.ts +1288 -0
- package/.pi/extensions/takomi-runtime/profile.ts +114 -0
- package/.pi/extensions/takomi-runtime/routing-policy.ts +105 -0
- package/.pi/extensions/takomi-runtime/shared.ts +492 -0
- package/.pi/extensions/takomi-runtime/subagent-controller.ts +364 -0
- package/.pi/extensions/takomi-runtime/subagent-render.ts +501 -0
- package/.pi/extensions/takomi-runtime/subagent-types.ts +83 -0
- package/.pi/extensions/takomi-runtime/ui.ts +133 -0
- package/.pi/extensions/takomi-subagents/agent-aliases.ts +18 -0
- package/.pi/extensions/takomi-subagents/agents.ts +113 -0
- package/.pi/extensions/takomi-subagents/delegation-plan.ts +95 -0
- package/.pi/extensions/takomi-subagents/dispatch-helpers.ts +26 -0
- package/.pi/extensions/takomi-subagents/dispatch.ts +215 -0
- package/.pi/extensions/takomi-subagents/index.ts +75 -0
- package/.pi/extensions/takomi-subagents/live-updates.ts +83 -0
- package/.pi/extensions/takomi-subagents/native-render.ts +174 -0
- package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -0
- package/.pi/prompts/build-prompt.md +199 -0
- package/.pi/prompts/design-prompt.md +134 -0
- package/.pi/prompts/genesis-prompt.md +133 -0
- package/.pi/prompts/orch-prompt.md +144 -0
- package/.pi/prompts/prime-prompt.md +80 -0
- package/.pi/prompts/takomi-prompt.md +96 -0
- package/.pi/prompts/vibe-primeAgent.md +97 -0
- package/.pi/prompts/vibe-spawnTask.md +133 -0
- package/.pi/prompts/vibe-syncDocs.md +100 -0
- package/.pi/themes/takomi-noir.json +81 -0
- package/README.md +28 -2
- package/assets/.agent/skills/pr-comment-fix/SKILL.md +182 -0
- package/assets/.agent/skills/takomi/SKILL.md +59 -59
- package/package.json +59 -46
- package/src/cli.js +158 -8
- package/src/doctor.js +84 -0
- package/src/pi-harness.js +351 -0
- package/src/pi-installer.js +171 -0
- package/src/pi-takomi-core/index.ts +4 -0
- package/src/pi-takomi-core/orchestration.ts +402 -0
- package/src/pi-takomi-core/routing.ts +93 -0
- package/src/pi-takomi-core/types.ts +173 -0
- package/src/pi-takomi-core/workflows.ts +299 -0
- package/src/skills-installer.js +101 -0
- package/src/utils.js +479 -447
- package/assets/.agent/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-311.pyc +0 -0
- package/assets/.agent/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
- package/assets/.agent/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-311.pyc +0 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import type { RouterConfig, RouterModelConfig, RouterUpstreamConfig } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export const EXTENSION_ROOT = join(homedir(), ".pi", "agent", "extensions", "oauth-router");
|
|
7
|
+
export const DATA_ROOT = join(homedir(), ".pi", "agent", "oauth-router");
|
|
8
|
+
export const CONFIG_PATH = join(DATA_ROOT, "config.json");
|
|
9
|
+
export const CREDENTIALS_PATH = join(DATA_ROOT, "credentials.json");
|
|
10
|
+
export const STATE_PATH = join(DATA_ROOT, "state.json");
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MODELS: RouterModelConfig[] = [
|
|
13
|
+
{
|
|
14
|
+
id: "gpt-4o",
|
|
15
|
+
name: "GPT-4o",
|
|
16
|
+
reasoning: false,
|
|
17
|
+
input: ["text", "image"],
|
|
18
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
19
|
+
contextWindow: 128000,
|
|
20
|
+
maxTokens: 16384,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "gpt-4.1",
|
|
24
|
+
name: "GPT-4.1",
|
|
25
|
+
reasoning: false,
|
|
26
|
+
input: ["text", "image"],
|
|
27
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
28
|
+
contextWindow: 128000,
|
|
29
|
+
maxTokens: 16384,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "o4-mini",
|
|
33
|
+
name: "o4-mini",
|
|
34
|
+
reasoning: true,
|
|
35
|
+
input: ["text", "image"],
|
|
36
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
37
|
+
contextWindow: 200000,
|
|
38
|
+
maxTokens: 100000,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "gpt-5.1",
|
|
42
|
+
name: "GPT-5.1",
|
|
43
|
+
reasoning: true,
|
|
44
|
+
input: ["text", "image"],
|
|
45
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
46
|
+
contextWindow: 272000,
|
|
47
|
+
maxTokens: 128000,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "gpt-5.4",
|
|
51
|
+
name: "GPT-5.4",
|
|
52
|
+
reasoning: true,
|
|
53
|
+
input: ["text", "image"],
|
|
54
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
55
|
+
contextWindow: 272000,
|
|
56
|
+
maxTokens: 128000,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "gpt-5.4-mini",
|
|
60
|
+
name: "GPT-5.4 Mini",
|
|
61
|
+
reasoning: true,
|
|
62
|
+
input: ["text", "image"],
|
|
63
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
64
|
+
contextWindow: 272000,
|
|
65
|
+
maxTokens: 128000,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "gpt-5.5",
|
|
69
|
+
name: "GPT-5.5",
|
|
70
|
+
reasoning: true,
|
|
71
|
+
input: ["text", "image"],
|
|
72
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
73
|
+
contextWindow: 272000,
|
|
74
|
+
maxTokens: 128000,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const DEFAULT_UPSTREAMS: RouterUpstreamConfig[] = [
|
|
79
|
+
{
|
|
80
|
+
id: "openai-compatible",
|
|
81
|
+
label: "OpenAI Compatible API",
|
|
82
|
+
description: "API-key fallback route for standard OpenAI-compatible responses endpoints.",
|
|
83
|
+
baseUrl: "https://api.openai.com/v1",
|
|
84
|
+
api: "openai-responses",
|
|
85
|
+
authMode: "api-key",
|
|
86
|
+
enabled: true,
|
|
87
|
+
modelIds: ["gpt-4o", "gpt-4.1", "o4-mini"],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "chatgpt-codex",
|
|
91
|
+
label: "ChatGPT Codex OAuth",
|
|
92
|
+
description: "OAuth-backed ChatGPT Plus/Pro Codex route using Pi's OpenAI Codex OAuth implementation.",
|
|
93
|
+
baseUrl: "https://chatgpt.com/backend-api",
|
|
94
|
+
api: "openai-codex-responses",
|
|
95
|
+
authMode: "oauth",
|
|
96
|
+
oauthProviderId: "openai-codex",
|
|
97
|
+
enabled: true,
|
|
98
|
+
modelIds: ["gpt-5.1", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5"],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
export const DEFAULT_CONFIG: RouterConfig = {
|
|
103
|
+
version: 1,
|
|
104
|
+
providerName: "oauth-router",
|
|
105
|
+
policy: "round-robin",
|
|
106
|
+
tokenRefreshSkewMs: 60_000,
|
|
107
|
+
rateLimitCooldownMs: 120_000,
|
|
108
|
+
transientPenaltyMs: 30_000,
|
|
109
|
+
models: DEFAULT_MODELS,
|
|
110
|
+
upstreams: DEFAULT_UPSTREAMS,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function applySecurePermissions(path: string, mode: number) {
|
|
114
|
+
try {
|
|
115
|
+
chmodSync(path, mode);
|
|
116
|
+
} catch {
|
|
117
|
+
// Best effort only. Windows commonly ignores POSIX chmod semantics.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function ensureDirectory(path: string) {
|
|
122
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
123
|
+
applySecurePermissions(path, 0o700);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function writeJsonFile(path: string, value: unknown, secure = true) {
|
|
127
|
+
ensureDirectory(dirname(path));
|
|
128
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: secure ? 0o600 : 0o644 });
|
|
129
|
+
applySecurePermissions(path, secure ? 0o600 : 0o644);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function deepClone<T>(value: T): T {
|
|
133
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function mergeModelConfigs(candidateModels: RouterModelConfig[] | undefined): RouterModelConfig[] {
|
|
137
|
+
if (!Array.isArray(candidateModels) || candidateModels.length === 0) {
|
|
138
|
+
return deepClone(DEFAULT_CONFIG.models);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const merged = new Map(DEFAULT_CONFIG.models.map((model) => [model.id, deepClone(model)]));
|
|
142
|
+
for (const model of candidateModels) {
|
|
143
|
+
const previous = merged.get(model.id) ?? ({} as RouterModelConfig);
|
|
144
|
+
merged.set(model.id, { ...previous, ...deepClone(model), id: model.id });
|
|
145
|
+
}
|
|
146
|
+
return Array.from(merged.values());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function mergeUpstreamConfigs(candidateUpstreams: RouterUpstreamConfig[] | undefined): RouterUpstreamConfig[] {
|
|
150
|
+
if (!Array.isArray(candidateUpstreams) || candidateUpstreams.length === 0) {
|
|
151
|
+
return deepClone(DEFAULT_CONFIG.upstreams);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const merged = new Map(DEFAULT_CONFIG.upstreams.map((upstream) => [upstream.id, deepClone(upstream)]));
|
|
155
|
+
for (const upstream of candidateUpstreams) {
|
|
156
|
+
const previous = merged.get(upstream.id);
|
|
157
|
+
if (!previous) {
|
|
158
|
+
merged.set(upstream.id, deepClone(upstream));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const modelIds = Array.from(new Set([...(previous.modelIds ?? []), ...(upstream.modelIds ?? [])]));
|
|
163
|
+
merged.set(upstream.id, { ...previous, ...deepClone(upstream), id: upstream.id, modelIds });
|
|
164
|
+
}
|
|
165
|
+
return Array.from(merged.values());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function mergeConfig(candidate: Partial<RouterConfig> | undefined): RouterConfig {
|
|
169
|
+
return {
|
|
170
|
+
...DEFAULT_CONFIG,
|
|
171
|
+
...candidate,
|
|
172
|
+
providerName: "oauth-router",
|
|
173
|
+
version: 1,
|
|
174
|
+
models: mergeModelConfigs(candidate?.models),
|
|
175
|
+
upstreams: mergeUpstreamConfigs(candidate?.upstreams),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function loadRouterConfig(): RouterConfig {
|
|
180
|
+
ensureDirectory(DATA_ROOT);
|
|
181
|
+
|
|
182
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
183
|
+
const initial = deepClone(DEFAULT_CONFIG);
|
|
184
|
+
writeJsonFile(CONFIG_PATH, initial, false);
|
|
185
|
+
return initial;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<RouterConfig>;
|
|
190
|
+
const merged = mergeConfig(parsed);
|
|
191
|
+
if (JSON.stringify(parsed) !== JSON.stringify(merged)) {
|
|
192
|
+
writeJsonFile(CONFIG_PATH, merged, false);
|
|
193
|
+
}
|
|
194
|
+
return merged;
|
|
195
|
+
} catch {
|
|
196
|
+
const fallback = deepClone(DEFAULT_CONFIG);
|
|
197
|
+
writeJsonFile(CONFIG_PATH, fallback, false);
|
|
198
|
+
return fallback;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { registerRouterCommands, formatStatusReport } from "./commands.ts";
|
|
3
|
+
import { registerRouterProvider, RouterRuntime } from "./provider.ts";
|
|
4
|
+
|
|
5
|
+
function updateFooter(pi: ExtensionAPI, runtime: RouterRuntime, notify = false) {
|
|
6
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
7
|
+
const rows = runtime.getStatusRows();
|
|
8
|
+
const healthy = rows.filter((row) => {
|
|
9
|
+
if (!row.enabled) return false;
|
|
10
|
+
if (row.authHealth !== "ok") return false;
|
|
11
|
+
if (row.cooldownUntil && row.cooldownUntil > Date.now()) return false;
|
|
12
|
+
if (row.penaltyUntil && row.penaltyUntil > Date.now()) return false;
|
|
13
|
+
return true;
|
|
14
|
+
}).length;
|
|
15
|
+
|
|
16
|
+
ctx.ui.setStatus("oauth-router", `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`);
|
|
17
|
+
if (notify) {
|
|
18
|
+
ctx.ui.notify("oauth-router loaded", "info");
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function (pi: ExtensionAPI) {
|
|
24
|
+
const runtime = new RouterRuntime();
|
|
25
|
+
|
|
26
|
+
registerRouterProvider(pi, runtime);
|
|
27
|
+
registerRouterCommands(pi, runtime);
|
|
28
|
+
updateFooter(pi, runtime, false);
|
|
29
|
+
|
|
30
|
+
pi.registerCommand("router-debug-report", {
|
|
31
|
+
description: "Emit a detailed oauth-router report",
|
|
32
|
+
handler: async () => {
|
|
33
|
+
pi.sendMessage({
|
|
34
|
+
customType: "oauth-router",
|
|
35
|
+
content: formatStatusReport(runtime),
|
|
36
|
+
display: true,
|
|
37
|
+
details: { source: "oauth-router", debug: true },
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|