takomi 2.1.31 → 2.1.33
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/extensions/oauth-router/README.md +2 -1
- package/.pi/extensions/oauth-router/config.ts +14 -5
- package/.pi/extensions/oauth-router/index.ts +105 -16
- package/.pi/extensions/oauth-router/provider.ts +144 -3
- package/.pi/extensions/oauth-router/state.ts +372 -372
- package/.pi/extensions/oauth-router/types.ts +19 -1
- package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +296 -296
- package/assets/.agent/skills/photo-book-builder/SKILL.md +96 -0
- package/assets/.agent/skills/photo-book-builder/references/layout_templates.md +72 -0
- package/assets/.agent/skills/photo-book-builder/scripts/create_full_bleed_layouts.py +212 -0
- package/assets/.agent/skills/photo-book-builder/scripts/organize_photos.py +99 -0
- package/assets/.agent/skills/photo-book-builder/scripts/revert_organization.py +61 -0
- package/assets/.agent/skills/photo-book-builder/scripts/upscale_covers.py +47 -0
- package/assets/.agent/skills/youtube-pipeline/SKILL.md +73 -62
- package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Application.md +1 -0
- package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phase 1.md +86 -0
- package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phase 2.md +106 -0
- package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phase 3.md +112 -0
- package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phase 4.md +90 -0
- package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Phased Outline.md +58 -0
- package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Repurposing.md +1 -0
- package/assets/.agent/skills/youtube-pipeline/resources/knowledge/Research.md +438 -0
- package/assets/.agent/skills/youtube-pipeline/resources/prompts/Shorts Bridge Protocol.md +159 -0
- package/assets/.agent/skills/youtube-pipeline/resources/prompts/Title Thumbnail Picker Prompt.md +144 -0
- package/assets/.agent/skills/youtube-pipeline/resources/prompts/Transcription Extraction Prompt v2.md +190 -0
- package/assets/.agent/skills/youtube-pipeline/resources/prompts/Transcription Extraction Prompt.md +156 -0
- package/assets/.agent/skills/youtube-pipeline/resources/prompts/Video QA Prompt.md +133 -0
- package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase1-strategy.md +28 -18
- package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase2-packaging.md +2 -2
- package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase3-scripting.md +2 -2
- package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase3.5-shorts.md +2 -2
- package/assets/.agent/skills/youtube-pipeline/resources/youtube-phase4-production.md +2 -2
- package/assets/.agent/skills/youtube-pipeline/resources/youtube-pipeline.md +15 -15
- package/assets/.agent/skills/youtube-pipeline/scripts/google-trends/package.json +17 -0
- package/assets/.agent/skills/youtube-pipeline/scripts/google-trends/pnpm-lock.yaml +31 -0
- package/assets/.agent/skills/youtube-pipeline/scripts/google-trends/search.js +168 -0
- package/package.json +1 -1
- package/src/cli.js +5 -0
- package/src/pi-harness.js +34 -8
- package/src/pi-optional-features.js +190 -195
- package/src/postinstall.js +27 -27
- package/src/skills-catalog.js +245 -245
- package/src/skills-installer.js +244 -244
- package/src/skills-selection-tui.js +200 -200
- package/src/store.js +418 -418
- package/src/takomi-stats.d.ts +3 -3
- package/src/takomi-stats.js +442 -35
|
@@ -28,6 +28,7 @@ Pi extension that auto-loads and registers an `oauth-router` provider with multi
|
|
|
28
28
|
- 429 cooldowns
|
|
29
29
|
- transient failure penalties
|
|
30
30
|
- client/network transport failure tracking without default account cooldown
|
|
31
|
+
- live Pi UI footer/notification updates when an upstream attempt, retry delay, failover, or final router error happens
|
|
31
32
|
- 5 router-level client/network retries by default before pre-output failover, starting at 5s and doubling
|
|
32
33
|
- auth failure quarantine
|
|
33
34
|
- safe pre-output failover
|
|
@@ -47,7 +48,7 @@ The extension ships with two default upstream profiles:
|
|
|
47
48
|
- api: `openai-responses`
|
|
48
49
|
- default models: `gpt-4o`, `gpt-4.1`, `o4-mini`
|
|
49
50
|
|
|
50
|
-
Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoints, change model catalogs, or tune retry behavior. By default client-side transport failures such as `Codex SSE response headers timed out after 10000ms` retry the same account 5 times with exponential backoff (`5s`, `10s`, `20s`, `40s`, then capped at `60s`) before router failover. These failures are recorded but do not cool down an account unless `clientNetworkPenaltyMs` is set above `0`.
|
|
51
|
+
Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoints, change model catalogs, or tune retry behavior. By default client-side transport failures such as `Codex SSE response headers timed out after 10000ms` retry the same account 5 times with exponential backoff (`5s`, `10s`, `20s`, `40s`, then capped at `60s`) before router failover. While this is happening, Pi's footer shows the active retry/failover/error state so the UI no longer looks frozen. These failures are recorded but do not cool down an account unless `clientNetworkPenaltyMs` is set above `0`.
|
|
51
52
|
|
|
52
53
|
## Setup
|
|
53
54
|
|
|
@@ -9,6 +9,10 @@ export const CONFIG_PATH = join(DATA_ROOT, "config.json");
|
|
|
9
9
|
export const CREDENTIALS_PATH = join(DATA_ROOT, "credentials.json");
|
|
10
10
|
export const STATE_PATH = join(DATA_ROOT, "state.json");
|
|
11
11
|
|
|
12
|
+
const LEGACY_CODEX_CONTEXT_WINDOW = 272000;
|
|
13
|
+
const SAFE_CODEX_CONTEXT_WINDOW = 240000;
|
|
14
|
+
const CODEX_MODEL_IDS = new Set(["gpt-5.1", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5"]);
|
|
15
|
+
|
|
12
16
|
const DEFAULT_MODELS: RouterModelConfig[] = [
|
|
13
17
|
{
|
|
14
18
|
id: "gpt-4o",
|
|
@@ -43,7 +47,7 @@ const DEFAULT_MODELS: RouterModelConfig[] = [
|
|
|
43
47
|
reasoning: true,
|
|
44
48
|
input: ["text", "image"],
|
|
45
49
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
46
|
-
contextWindow:
|
|
50
|
+
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
47
51
|
maxTokens: 128000,
|
|
48
52
|
},
|
|
49
53
|
{
|
|
@@ -52,7 +56,7 @@ const DEFAULT_MODELS: RouterModelConfig[] = [
|
|
|
52
56
|
reasoning: true,
|
|
53
57
|
input: ["text", "image"],
|
|
54
58
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
55
|
-
contextWindow:
|
|
59
|
+
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
56
60
|
maxTokens: 128000,
|
|
57
61
|
},
|
|
58
62
|
{
|
|
@@ -61,7 +65,7 @@ const DEFAULT_MODELS: RouterModelConfig[] = [
|
|
|
61
65
|
reasoning: true,
|
|
62
66
|
input: ["text", "image"],
|
|
63
67
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
64
|
-
contextWindow:
|
|
68
|
+
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
65
69
|
maxTokens: 128000,
|
|
66
70
|
},
|
|
67
71
|
{
|
|
@@ -70,7 +74,7 @@ const DEFAULT_MODELS: RouterModelConfig[] = [
|
|
|
70
74
|
reasoning: true,
|
|
71
75
|
input: ["text", "image"],
|
|
72
76
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
73
|
-
contextWindow:
|
|
77
|
+
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
74
78
|
maxTokens: 128000,
|
|
75
79
|
},
|
|
76
80
|
];
|
|
@@ -210,7 +214,12 @@ function mergeModelConfigs(candidateModels: RouterModelConfig[] | undefined): Ro
|
|
|
210
214
|
const previous = merged.get(model.id) ?? ({} as RouterModelConfig);
|
|
211
215
|
merged.set(model.id, { ...previous, ...deepClone(model), id: model.id });
|
|
212
216
|
}
|
|
213
|
-
return Array.from(merged.values())
|
|
217
|
+
return Array.from(merged.values()).map((model) => {
|
|
218
|
+
if (CODEX_MODEL_IDS.has(model.id) && model.contextWindow === LEGACY_CODEX_CONTEXT_WINDOW) {
|
|
219
|
+
return { ...model, contextWindow: SAFE_CODEX_CONTEXT_WINDOW };
|
|
220
|
+
}
|
|
221
|
+
return model;
|
|
222
|
+
});
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
function mergeUpstreamConfigs(candidateUpstreams: RouterUpstreamConfig[] | undefined): RouterUpstreamConfig[] {
|
|
@@ -1,23 +1,112 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { registerRouterCommands, formatStatusReport } from "./commands.ts";
|
|
3
3
|
import { registerRouterProvider, RouterRuntime } from "./provider.ts";
|
|
4
|
+
import type { RouterUiEvent } from "./types.ts";
|
|
4
5
|
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
function getHealthSummary(runtime: RouterRuntime) {
|
|
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
|
+
return `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatDuration(ms?: number): string {
|
|
20
|
+
if (!ms || ms <= 0) return "now";
|
|
21
|
+
if (ms < 1_000) return `${Math.round(ms)}ms`;
|
|
22
|
+
return `${Math.round(ms / 1_000)}s`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function truncate(text: string | undefined, max = 110): string {
|
|
26
|
+
if (!text) return "";
|
|
27
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
28
|
+
return clean.length > max ? `${clean.slice(0, Math.max(0, max - 1))}…` : clean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatRoute(event: RouterUiEvent): string {
|
|
32
|
+
const parts = [event.accountLabel || event.accountId, event.upstreamId].filter(Boolean);
|
|
33
|
+
return parts.length ? parts.join(" @ ") : "router";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatActivityStatus(event: RouterUiEvent): string {
|
|
37
|
+
const route = formatRoute(event);
|
|
38
|
+
const model = event.modelId ? `${event.modelId} ` : "";
|
|
39
|
+
const reason = truncate(event.message, 70);
|
|
40
|
+
|
|
41
|
+
switch (event.phase) {
|
|
42
|
+
case "attempt": {
|
|
43
|
+
const retry = event.retryAttempt && event.maxRetries
|
|
44
|
+
? ` retry ${event.retryAttempt}/${event.maxRetries}`
|
|
45
|
+
: "";
|
|
46
|
+
return `oauth-router contacting ${model}via ${route}${retry}`;
|
|
47
|
+
}
|
|
48
|
+
case "retry":
|
|
49
|
+
return `oauth-router retry ${event.retryAttempt}/${event.maxRetries} in ${formatDuration(event.delayMs)} | ${reason || event.failureKind || "network error"}`;
|
|
50
|
+
case "failover":
|
|
51
|
+
return `oauth-router failover from ${route} | ${reason || event.failureKind || "upstream error"}`;
|
|
52
|
+
case "success":
|
|
53
|
+
return `oauth-router ok: ${model}via ${route}`;
|
|
54
|
+
case "error":
|
|
55
|
+
return `oauth-router error | ${reason || event.failureKind || "request failed"}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function notifyActivity(ctx: ExtensionContext, event: RouterUiEvent) {
|
|
60
|
+
if (event.phase === "attempt" || event.phase === "success") return;
|
|
61
|
+
|
|
62
|
+
const message = formatActivityStatus(event);
|
|
63
|
+
const level = event.phase === "error" ? "error" : "info";
|
|
64
|
+
ctx.ui.notify(message, level);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyOnLoad = false) {
|
|
68
|
+
let activeCtx: ExtensionContext | undefined;
|
|
69
|
+
|
|
70
|
+
const setBaseStatus = (ctx: ExtensionContext) => {
|
|
71
|
+
activeCtx = ctx;
|
|
72
|
+
if (ctx.hasUI) ctx.ui.setStatus("oauth-router", getHealthSummary(runtime));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
runtime.setUiReporter((event) => {
|
|
76
|
+
const ctx = activeCtx;
|
|
77
|
+
if (!ctx?.hasUI) return;
|
|
78
|
+
try {
|
|
79
|
+
ctx.ui.setStatus("oauth-router", formatActivityStatus(event));
|
|
80
|
+
notifyActivity(ctx, event);
|
|
81
|
+
} catch {
|
|
82
|
+
// Best-effort UI only; never disturb provider streaming.
|
|
19
83
|
}
|
|
20
84
|
});
|
|
85
|
+
|
|
86
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
87
|
+
setBaseStatus(ctx);
|
|
88
|
+
if (notifyOnLoad) ctx.ui.notify("oauth-router loaded", "info");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
92
|
+
setBaseStatus(ctx);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
96
|
+
setBaseStatus(ctx);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
100
|
+
setBaseStatus(ctx);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
104
|
+
setBaseStatus(ctx);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
pi.on("session_shutdown", async () => {
|
|
108
|
+
activeCtx = undefined;
|
|
109
|
+
});
|
|
21
110
|
}
|
|
22
111
|
|
|
23
112
|
export default function (pi: ExtensionAPI) {
|
|
@@ -25,7 +114,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
25
114
|
|
|
26
115
|
registerRouterProvider(pi, runtime);
|
|
27
116
|
registerRouterCommands(pi, runtime);
|
|
28
|
-
|
|
117
|
+
installRouterUiBridge(pi, runtime, false);
|
|
29
118
|
|
|
30
119
|
pi.registerCommand("router-debug-report", {
|
|
31
120
|
description: "Emit a detailed oauth-router report",
|
|
@@ -25,6 +25,8 @@ import type {
|
|
|
25
25
|
RouterErrorMessageInput,
|
|
26
26
|
RouterModelConfig,
|
|
27
27
|
RouterStatusRow,
|
|
28
|
+
RouterUiEvent,
|
|
29
|
+
RouterUiReporter,
|
|
28
30
|
RouterUpstreamConfig,
|
|
29
31
|
RoutingPolicyName,
|
|
30
32
|
StoredRouterAccount,
|
|
@@ -122,6 +124,20 @@ function isClientNetworkFailure(lower: string): boolean {
|
|
|
122
124
|
].some((token) => lower.includes(token));
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
function isContextOverflowFailure(lower: string): boolean {
|
|
128
|
+
return [
|
|
129
|
+
"context_length_exceeded",
|
|
130
|
+
"context length exceeded",
|
|
131
|
+
"context window exceeded",
|
|
132
|
+
"exceeds the context window",
|
|
133
|
+
"exceeds the model's maximum context length",
|
|
134
|
+
"exceeds model's maximum context length",
|
|
135
|
+
"maximum context length",
|
|
136
|
+
"too many tokens",
|
|
137
|
+
"token limit exceeded",
|
|
138
|
+
].some((token) => lower.includes(token));
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
function classifyFailure(
|
|
126
142
|
status: number | undefined,
|
|
127
143
|
headers: Record<string, string>,
|
|
@@ -135,6 +151,10 @@ function classifyFailure(
|
|
|
135
151
|
return { kind: "rate-limit", status: status ?? 429, retryAfterMs, message };
|
|
136
152
|
}
|
|
137
153
|
|
|
154
|
+
if (isContextOverflowFailure(lower)) {
|
|
155
|
+
return { kind: "context-overflow", status, message };
|
|
156
|
+
}
|
|
157
|
+
|
|
138
158
|
if (status === 401 || status === 403 || lower.includes("unauthorized") || lower.includes("forbidden")) {
|
|
139
159
|
return { kind: "auth", status: status ?? 401, message };
|
|
140
160
|
}
|
|
@@ -176,6 +196,7 @@ export class RouterRuntime {
|
|
|
176
196
|
private config: RouterConfig;
|
|
177
197
|
private accounts: RouterAccountStore;
|
|
178
198
|
private state: RouterStateStore;
|
|
199
|
+
private uiReporter?: RouterUiReporter;
|
|
179
200
|
|
|
180
201
|
constructor() {
|
|
181
202
|
this.config = loadRouterConfig();
|
|
@@ -190,6 +211,18 @@ export class RouterRuntime {
|
|
|
190
211
|
this.state.pruneAccountIds(this.accounts.list().map((account) => account.id));
|
|
191
212
|
}
|
|
192
213
|
|
|
214
|
+
setUiReporter(reporter?: RouterUiReporter) {
|
|
215
|
+
this.uiReporter = reporter;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private emitUi(event: RouterUiEvent) {
|
|
219
|
+
try {
|
|
220
|
+
this.uiReporter?.(event);
|
|
221
|
+
} catch {
|
|
222
|
+
// UI updates must never affect model streaming or retry behavior.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
193
226
|
getConfig(): RouterConfig {
|
|
194
227
|
this.reloadConfig();
|
|
195
228
|
return this.config;
|
|
@@ -446,6 +479,11 @@ export class RouterRuntime {
|
|
|
446
479
|
this.state.recordClientNetworkFailure(accountId, failure.status, failure.message);
|
|
447
480
|
}
|
|
448
481
|
break;
|
|
482
|
+
case "context-overflow":
|
|
483
|
+
// Context-size failures are request-level, not account-health failures.
|
|
484
|
+
// Do not cool down or quarantine accounts; Pi needs the overflow error surfaced
|
|
485
|
+
// directly so its built-in compaction recovery can compact and retry.
|
|
486
|
+
break;
|
|
449
487
|
case "fatal":
|
|
450
488
|
this.state.markTransientFailure(accountId, this.config.transientPenaltyMs, failure.status ?? 500, failure.message);
|
|
451
489
|
break;
|
|
@@ -468,17 +506,40 @@ export class RouterRuntime {
|
|
|
468
506
|
): Promise<{ completed: boolean; emittedMeaningfulOutput: boolean; failure?: FailureClassification }> {
|
|
469
507
|
for (let retryIndex = 0; ; retryIndex += 1) {
|
|
470
508
|
const result = await this.trySingleAccount(selection, outerStream, context, options);
|
|
509
|
+
const failure = result.failure;
|
|
471
510
|
const canRetry =
|
|
472
511
|
!result.completed &&
|
|
473
512
|
!result.emittedMeaningfulOutput &&
|
|
474
|
-
|
|
513
|
+
failure?.kind === "client-network" &&
|
|
475
514
|
retryIndex < this.config.clientNetworkMaxRetries;
|
|
476
515
|
|
|
477
|
-
if (!canRetry) return result;
|
|
516
|
+
if (!canRetry || !failure) return result;
|
|
478
517
|
|
|
479
518
|
const delayMs = this.getClientNetworkRetryDelayMs(retryIndex);
|
|
519
|
+
this.emitUi({
|
|
520
|
+
phase: "retry",
|
|
521
|
+
modelId,
|
|
522
|
+
accountId: selection.account.id,
|
|
523
|
+
accountLabel: selection.account.label,
|
|
524
|
+
upstreamId: selection.account.upstreamId,
|
|
525
|
+
failureKind: failure.kind,
|
|
526
|
+
status: failure.status,
|
|
527
|
+
message: failure.message,
|
|
528
|
+
retryAttempt: retryIndex + 1,
|
|
529
|
+
maxRetries: this.config.clientNetworkMaxRetries,
|
|
530
|
+
delayMs,
|
|
531
|
+
});
|
|
480
532
|
await sleep(delayMs, options?.signal);
|
|
481
533
|
this.state.markAttempt(selection.account.id, modelId);
|
|
534
|
+
this.emitUi({
|
|
535
|
+
phase: "attempt",
|
|
536
|
+
modelId,
|
|
537
|
+
accountId: selection.account.id,
|
|
538
|
+
accountLabel: selection.account.label,
|
|
539
|
+
upstreamId: selection.account.upstreamId,
|
|
540
|
+
retryAttempt: retryIndex + 2,
|
|
541
|
+
maxRetries: this.config.clientNetworkMaxRetries,
|
|
542
|
+
});
|
|
482
543
|
}
|
|
483
544
|
}
|
|
484
545
|
|
|
@@ -521,10 +582,20 @@ export class RouterRuntime {
|
|
|
521
582
|
reason: "aborted",
|
|
522
583
|
error: { ...event.error, stopReason: "aborted", errorMessage: message },
|
|
523
584
|
});
|
|
524
|
-
return {
|
|
585
|
+
return {
|
|
586
|
+
completed: true,
|
|
587
|
+
emittedMeaningfulOutput,
|
|
588
|
+
failure: { kind: "fatal", status: responseStatus, message },
|
|
589
|
+
};
|
|
525
590
|
}
|
|
526
591
|
|
|
527
592
|
const failure = classifyFailure(responseStatus, responseHeaders, message, this.config);
|
|
593
|
+
|
|
594
|
+
if (failure.kind === "context-overflow") {
|
|
595
|
+
outerStream.push(event);
|
|
596
|
+
return { completed: true, emittedMeaningfulOutput, failure };
|
|
597
|
+
}
|
|
598
|
+
|
|
528
599
|
this.markFailure(selection.account.id, failure);
|
|
529
600
|
|
|
530
601
|
if (!emittedMeaningfulOutput) {
|
|
@@ -596,6 +667,13 @@ export class RouterRuntime {
|
|
|
596
667
|
const message = lastFailure
|
|
597
668
|
? `No healthy oauth-router accounts remaining after failover. Last error: ${lastFailure.message}`
|
|
598
669
|
: `No healthy oauth-router accounts are configured for model ${model.id}. Add accounts with /router-login add.`;
|
|
670
|
+
this.emitUi({
|
|
671
|
+
phase: "error",
|
|
672
|
+
modelId: model.id,
|
|
673
|
+
failureKind: lastFailure?.kind,
|
|
674
|
+
status: lastFailure?.status,
|
|
675
|
+
message,
|
|
676
|
+
});
|
|
599
677
|
outer.push({ type: "error", reason: "error", error: createErrorMessage({ model, message }) });
|
|
600
678
|
outer.end();
|
|
601
679
|
return;
|
|
@@ -604,6 +682,13 @@ export class RouterRuntime {
|
|
|
604
682
|
this.state.advanceCursor(policy, picked.nextCursor);
|
|
605
683
|
tried.add(picked.selected.account.id);
|
|
606
684
|
this.state.markAttempt(picked.selected.account.id, model.id);
|
|
685
|
+
this.emitUi({
|
|
686
|
+
phase: "attempt",
|
|
687
|
+
modelId: model.id,
|
|
688
|
+
accountId: picked.selected.account.id,
|
|
689
|
+
accountLabel: picked.selected.account.label,
|
|
690
|
+
upstreamId: picked.selected.account.upstreamId,
|
|
691
|
+
});
|
|
607
692
|
|
|
608
693
|
let selection: DelegateSelection;
|
|
609
694
|
try {
|
|
@@ -617,11 +702,39 @@ export class RouterRuntime {
|
|
|
617
702
|
}
|
|
618
703
|
lastFailure = { kind: "auth", status: 401, message };
|
|
619
704
|
this.markFailure(picked.selected.account.id, lastFailure);
|
|
705
|
+
this.emitUi({
|
|
706
|
+
phase: "failover",
|
|
707
|
+
modelId: model.id,
|
|
708
|
+
accountId: picked.selected.account.id,
|
|
709
|
+
accountLabel: picked.selected.account.label,
|
|
710
|
+
upstreamId: picked.selected.account.upstreamId,
|
|
711
|
+
failureKind: lastFailure.kind,
|
|
712
|
+
status: lastFailure.status,
|
|
713
|
+
message: lastFailure.message,
|
|
714
|
+
});
|
|
620
715
|
continue;
|
|
621
716
|
}
|
|
622
717
|
|
|
623
718
|
const result = await this.tryAccountWithClientNetworkRetries(selection, outer, context, model.id, options);
|
|
624
719
|
if (result.completed) {
|
|
720
|
+
this.emitUi(result.failure
|
|
721
|
+
? {
|
|
722
|
+
phase: "error",
|
|
723
|
+
modelId: model.id,
|
|
724
|
+
accountId: selection.account.id,
|
|
725
|
+
accountLabel: selection.account.label,
|
|
726
|
+
upstreamId: selection.account.upstreamId,
|
|
727
|
+
failureKind: result.failure.kind,
|
|
728
|
+
status: result.failure.status,
|
|
729
|
+
message: result.failure.message,
|
|
730
|
+
}
|
|
731
|
+
: {
|
|
732
|
+
phase: "success",
|
|
733
|
+
modelId: model.id,
|
|
734
|
+
accountId: selection.account.id,
|
|
735
|
+
accountLabel: selection.account.label,
|
|
736
|
+
upstreamId: selection.account.upstreamId,
|
|
737
|
+
});
|
|
625
738
|
outer.end();
|
|
626
739
|
return;
|
|
627
740
|
}
|
|
@@ -630,13 +743,41 @@ export class RouterRuntime {
|
|
|
630
743
|
lastFailure = result.failure;
|
|
631
744
|
|
|
632
745
|
if (emittedMeaningfulOutput) {
|
|
746
|
+
this.emitUi({
|
|
747
|
+
phase: "error",
|
|
748
|
+
modelId: model.id,
|
|
749
|
+
accountId: selection.account.id,
|
|
750
|
+
accountLabel: selection.account.label,
|
|
751
|
+
upstreamId: selection.account.upstreamId,
|
|
752
|
+
failureKind: lastFailure?.kind,
|
|
753
|
+
status: lastFailure?.status,
|
|
754
|
+
message: lastFailure?.message ?? "Upstream failed after partial output; failover is unsafe.",
|
|
755
|
+
});
|
|
633
756
|
outer.end();
|
|
634
757
|
return;
|
|
635
758
|
}
|
|
759
|
+
|
|
760
|
+
if (lastFailure) {
|
|
761
|
+
this.emitUi({
|
|
762
|
+
phase: "failover",
|
|
763
|
+
modelId: model.id,
|
|
764
|
+
accountId: selection.account.id,
|
|
765
|
+
accountLabel: selection.account.label,
|
|
766
|
+
upstreamId: selection.account.upstreamId,
|
|
767
|
+
failureKind: lastFailure.kind,
|
|
768
|
+
status: lastFailure.status,
|
|
769
|
+
message: lastFailure.message,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
636
772
|
}
|
|
637
773
|
} catch (error) {
|
|
638
774
|
const message = error instanceof Error ? error.message : String(error);
|
|
639
775
|
const aborted = isAbortLike(message, options?.signal);
|
|
776
|
+
this.emitUi({
|
|
777
|
+
phase: "error",
|
|
778
|
+
modelId: model.id,
|
|
779
|
+
message: aborted ? "Request was aborted" : message,
|
|
780
|
+
});
|
|
640
781
|
outer.push({ type: "error", reason: aborted ? "aborted" : "error", error: createErrorMessage({ model, message, stopReason: aborted ? "aborted" : "error" }) });
|
|
641
782
|
outer.end();
|
|
642
783
|
}
|