@hicaru/pi-rlm 0.3.14 → 0.3.16
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 +100 -54
- package/README.ru.md +69 -0
- package/README.zh-CN.md +66 -0
- package/package.json +1 -1
- package/src/bridge/handlers/emitting.ts +8 -0
- package/src/bridge/handlers/rlm-query.ts +2 -0
- package/src/bridge/model.ts +15 -1
- package/src/config/defaults.ts +3 -0
- package/src/config/settings.ts +6 -2
- package/src/core/answer.ts +5 -2
- package/src/core/budget.ts +22 -0
- package/src/core/engine.ts +91 -16
- package/src/core/types.ts +5 -0
- package/src/prompts/user.ts +25 -0
- package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
- package/src/sandbox/py/__pycache__/worker.cpython-314.pyc +0 -0
- package/src/sandbox/py/retrieval.py +31 -2
- package/src/sandbox/py/worker.py +30 -4
- package/src/sandbox/sandbox.ts +14 -1
- package/src/text/parsing.ts +28 -7
- package/src/tool/repl-tool.ts +2 -0
- package/src/tool/rlm-aggregator.ts +1 -1
- package/src/tool/rlm-details.ts +4 -1
- package/src/tool/rlm-events.ts +7 -2
- package/src/tool/rlm-tool.ts +3 -1
- package/src/tool/subcall-render.ts +10 -4
- package/src/tool/subcall-store.ts +32 -5
- package/src/ui/config-panel.ts +39 -0
- package/src/ui/panel/run-registry.ts +8 -0
- package/src/ui/theme.ts +6 -0
- package/src/ui/tree/tree-model.ts +19 -1
- package/src/ui/tree/tree-rows.ts +8 -8
- package/src/util/retry.ts +6 -3
|
@@ -18,6 +18,9 @@ type MutableSubcall = {
|
|
|
18
18
|
export interface SubcallTotals {
|
|
19
19
|
readonly costUsd: number;
|
|
20
20
|
readonly tokens: number;
|
|
21
|
+
/** In/out split (input / output) — mirrors tokens, shown separately in the tree. */
|
|
22
|
+
readonly tokensIn: number;
|
|
23
|
+
readonly tokensOut: number;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export class SubcallStore extends EmitterListener {
|
|
@@ -25,8 +28,12 @@ export class SubcallStore extends EmitterListener {
|
|
|
25
28
|
|
|
26
29
|
private totalCostUsd = 0;
|
|
27
30
|
private totalTokens = 0;
|
|
31
|
+
private totalTokensIn = 0;
|
|
32
|
+
private totalTokensOut = 0;
|
|
28
33
|
private rootCostUsd = 0;
|
|
29
34
|
private rootTokens = 0;
|
|
35
|
+
private rootTokensIn = 0;
|
|
36
|
+
private rootTokensOut = 0;
|
|
30
37
|
|
|
31
38
|
constructor(emitter: RlmEmitter, private readonly onChange?: () => void) {
|
|
32
39
|
super();
|
|
@@ -52,6 +59,8 @@ export class SubcallStore extends EmitterListener {
|
|
|
52
59
|
startedAt: Date.now(),
|
|
53
60
|
costUsd: 0,
|
|
54
61
|
tokens: 0,
|
|
62
|
+
tokensIn: 0,
|
|
63
|
+
tokensOut: 0,
|
|
55
64
|
});
|
|
56
65
|
}
|
|
57
66
|
|
|
@@ -75,6 +84,14 @@ export class SubcallStore extends EmitterListener {
|
|
|
75
84
|
sc.tokens += event.tokens;
|
|
76
85
|
this.totalTokens += event.tokens;
|
|
77
86
|
}
|
|
87
|
+
if (event.tokensIn !== undefined) {
|
|
88
|
+
sc.tokensIn += event.tokensIn;
|
|
89
|
+
this.totalTokensIn += event.tokensIn;
|
|
90
|
+
}
|
|
91
|
+
if (event.tokensOut !== undefined) {
|
|
92
|
+
sc.tokensOut += event.tokensOut;
|
|
93
|
+
this.totalTokensOut += event.tokensOut;
|
|
94
|
+
}
|
|
78
95
|
if (event.failedCount !== undefined) sc.failedCount = event.failedCount;
|
|
79
96
|
if (event.totalCount !== undefined) sc.totalCount = event.totalCount;
|
|
80
97
|
}
|
|
@@ -87,8 +104,8 @@ export class SubcallStore extends EmitterListener {
|
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
/** Snapshot running totals. O(1). */
|
|
90
|
-
getTotals():
|
|
91
|
-
return { costUsd: this.totalCostUsd, tokens: this.totalTokens };
|
|
107
|
+
getTotals(): SubcallTotals {
|
|
108
|
+
return { costUsd: this.totalCostUsd, tokens: this.totalTokens, tokensIn: this.totalTokensIn, tokensOut: this.totalTokensOut };
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
/**
|
|
@@ -126,33 +143,43 @@ export class SubcallStore extends EmitterListener {
|
|
|
126
143
|
const taken: RlmSubcall[] = [];
|
|
127
144
|
let costUsd = 0;
|
|
128
145
|
let tokens = 0;
|
|
146
|
+
let tokensIn = 0;
|
|
147
|
+
let tokensOut = 0;
|
|
129
148
|
for (const root of children.get(undefined) ?? []) {
|
|
130
149
|
const subtree = settledSubtree(root);
|
|
131
150
|
if (subtree === undefined) continue;
|
|
132
151
|
for (const node of subtree) {
|
|
133
152
|
costUsd += node.costUsd;
|
|
134
153
|
tokens += node.tokens;
|
|
154
|
+
tokensIn += node.tokensIn;
|
|
155
|
+
tokensOut += node.tokensOut;
|
|
135
156
|
taken.push(Object.freeze({ ...node, status: node.status as SubcallStatus }));
|
|
136
157
|
this.subcalls.delete(node.id);
|
|
137
158
|
}
|
|
138
159
|
}
|
|
139
160
|
this.totalCostUsd -= costUsd;
|
|
140
161
|
this.totalTokens -= tokens;
|
|
141
|
-
|
|
162
|
+
this.totalTokensIn -= tokensIn;
|
|
163
|
+
this.totalTokensOut -= tokensOut;
|
|
164
|
+
return { subcalls: taken, totals: { costUsd, tokens, tokensIn, tokensOut } };
|
|
142
165
|
}
|
|
143
166
|
|
|
144
167
|
// ── Root usage (delegated from RlmEventAggregator) ──
|
|
145
168
|
|
|
146
169
|
/** Accumulate root-level usage into shared totals. Called by aggregator. */
|
|
147
|
-
addRootUsage(costUsd: number, tokens: number): void {
|
|
170
|
+
addRootUsage(costUsd: number, tokens: number, tokensIn = 0, tokensOut = 0): void {
|
|
148
171
|
this.totalCostUsd += costUsd;
|
|
149
172
|
this.totalTokens += tokens;
|
|
173
|
+
this.totalTokensIn += tokensIn;
|
|
174
|
+
this.totalTokensOut += tokensOut;
|
|
150
175
|
this.rootCostUsd += costUsd;
|
|
151
176
|
this.rootTokens += tokens;
|
|
177
|
+
this.rootTokensIn += tokensIn;
|
|
178
|
+
this.rootTokensOut += tokensOut;
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
/** Root engine's OWN spend (driver-model turns only) — never blends sub-call models. */
|
|
155
182
|
getRootUsage(): SubcallTotals {
|
|
156
|
-
return { costUsd: this.rootCostUsd, tokens: this.rootTokens };
|
|
183
|
+
return { costUsd: this.rootCostUsd, tokens: this.rootTokens, tokensIn: this.rootTokensIn, tokensOut: this.rootTokensOut };
|
|
157
184
|
}
|
|
158
185
|
}
|
package/src/ui/config-panel.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** Config panel TUI — toggle RLM run parameters with descriptions. */
|
|
2
2
|
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
5
|
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
6
7
|
import type { RlmConfig } from "../core/types.ts";
|
|
8
|
+
import { THINKING_LEVELS } from "../config/settings.ts";
|
|
7
9
|
|
|
8
10
|
const CHOICES = Object.freeze({
|
|
9
11
|
maxDepth: Object.freeze(["1", "2", "3", "4"]),
|
|
@@ -18,6 +20,10 @@ const CHOICES = Object.freeze({
|
|
|
18
20
|
compaction: Object.freeze(["on", "off"]),
|
|
19
21
|
compactionThresholdPct: Object.freeze(["50", "65", "80", "90"]),
|
|
20
22
|
rootSamplingMaxTokens: Object.freeze(["4096", "8192", "16384", "32768"]),
|
|
23
|
+
rootSamplingTemperature: Object.freeze(["0", "0.3", "0.7", "1.0", "default"]),
|
|
24
|
+
smartReasoning: Object.freeze(["default", ...Object.keys(THINKING_LEVELS)]),
|
|
25
|
+
subSamplingMaxTokens: Object.freeze(["1024", "2048", "4096", "8192"]),
|
|
26
|
+
subSamplingTemperature: Object.freeze(["0", "0.3", "0.7", "1.0", "default"]),
|
|
21
27
|
sandboxInitTimeoutMs: Object.freeze(["10000", "30000", "60000", "120000"]),
|
|
22
28
|
requestTimeoutMs: Object.freeze(["2", "5", "10", "15", "20"]),
|
|
23
29
|
contextLoader: Object.freeze(["on", "off"]),
|
|
@@ -48,6 +54,14 @@ export async function showConfigPanel(ctx: ExtensionContext, config: RlmConfig):
|
|
|
48
54
|
item("compaction", "Trajectory compaction", config.compaction ? "on" : "off", CHOICES.compaction, "Summarize old turns when history approaches the model context window."),
|
|
49
55
|
item("compactionThresholdPct", "Compaction threshold (%)", String(Math.round(config.compactionThresholdPct * 100)), CHOICES.compactionThresholdPct, "Compact once estimated history tokens reach this share of the root model's context window."),
|
|
50
56
|
item("rootSamplingMaxTokens", "Root model output cap (tok)", String(config.rootSampling?.maxTokens ?? 16384), CHOICES.rootSamplingMaxTokens, "Max output tokens per root-model turn. Lower values keep each turn lean."),
|
|
57
|
+
item("rootSamplingTemperature", "Root sampling temperature", config.rootSampling?.temperature === undefined ? "default" : String(config.rootSampling?.temperature), CHOICES.rootSamplingTemperature,
|
|
58
|
+
"Sampling temperature for RLM root turns, finalize included — 0 = deterministic (the r3 reproducibility setting); 'default' = provider default. Applies to RLM-mode runs, rlm() delegation and child recursion; the native Pi agent loop follows Pi's own session settings."),
|
|
59
|
+
item("smartReasoning", "Root reasoning effort", config.smartReasoning ?? "default", CHOICES.smartReasoning,
|
|
60
|
+
"Thinking effort for the root model ('default' = none). Only models whose registry entry supports reasoning will think; others silently run without it. Reasoning tokens share the output cap — raise the root output cap when thinking is on."),
|
|
61
|
+
item("subSamplingMaxTokens", "Worker output cap (tok)", String(config.subSampling?.maxTokens ?? 8192), CHOICES.subSamplingMaxTokens,
|
|
62
|
+
"Max output tokens per leaf sub-call (llm_query / llm_batch / map_files)."),
|
|
63
|
+
item("subSamplingTemperature", "Worker sampling temperature", config.subSampling?.temperature === undefined ? "default" : String(config.subSampling?.temperature), CHOICES.subSamplingTemperature,
|
|
64
|
+
"Sampling temperature for leaf sub-calls; 'default' = provider default. Deterministic extraction (temp 0) is what made the r3 bench stable."),
|
|
51
65
|
item("sandboxInitTimeoutMs", "Sandbox init timeout", String(config.sandboxInitTimeoutMs), CHOICES.sandboxInitTimeoutMs, "How long to wait for the Python worker to start."),
|
|
52
66
|
item("requestTimeoutMs", "Sandbox request timeout (min)", String(Math.round(config.requestTimeoutMs / 60_000)), CHOICES.requestTimeoutMs, "Parent-side watchdog per sandbox request; on breach the Python worker is killed."),
|
|
53
67
|
item("contextLoader", "Context loader", config.contextLoader ? "on" : "off", CHOICES.contextLoader,
|
|
@@ -89,6 +103,13 @@ function optionalNumber(value: string, scale = 1): number | undefined {
|
|
|
89
103
|
return value === "none" ? undefined : Number(value) * scale;
|
|
90
104
|
}
|
|
91
105
|
|
|
106
|
+
/** Optional temperature: the literal "default" clears it (provider default); else [0, 2]. */
|
|
107
|
+
function optionalTemperature(value: string): number | undefined {
|
|
108
|
+
if (value === "default") return undefined;
|
|
109
|
+
const n = Number(value);
|
|
110
|
+
return Number.isFinite(n) && n >= 0 && n <= 2 ? n : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
/** Pure: returns a new frozen config with `id` set to `value`; unknown ids pass through. */
|
|
93
114
|
export function applySetting(config: RlmConfig, id: string, value: string): RlmConfig {
|
|
94
115
|
switch (id) {
|
|
@@ -105,6 +126,24 @@ export function applySetting(config: RlmConfig, id: string, value: string): RlmC
|
|
|
105
126
|
case "compactionThresholdPct": return Object.freeze({ ...config, compactionThresholdPct: Number(value) / 100 });
|
|
106
127
|
case "rootSamplingMaxTokens":
|
|
107
128
|
return Object.freeze({ ...config, rootSampling: Object.freeze({ ...config.rootSampling, maxTokens: Number(value) }) });
|
|
129
|
+
case "rootSamplingTemperature": {
|
|
130
|
+
const t = optionalTemperature(value);
|
|
131
|
+
// Reject invalid values (NaN / out of range) — keep the current setting.
|
|
132
|
+
if (t === undefined && value !== "default") return config;
|
|
133
|
+
return Object.freeze({ ...config, rootSampling: Object.freeze({ ...config.rootSampling, temperature: t }) });
|
|
134
|
+
}
|
|
135
|
+
case "smartReasoning":
|
|
136
|
+
if (value === "default") return Object.freeze({ ...config, smartReasoning: undefined });
|
|
137
|
+
return Object.hasOwn(THINKING_LEVELS, value)
|
|
138
|
+
? Object.freeze({ ...config, smartReasoning: value as ThinkingLevel })
|
|
139
|
+
: config;
|
|
140
|
+
case "subSamplingMaxTokens":
|
|
141
|
+
return Object.freeze({ ...config, subSampling: Object.freeze({ ...config.subSampling, maxTokens: Number(value) }) });
|
|
142
|
+
case "subSamplingTemperature": {
|
|
143
|
+
const st = optionalTemperature(value);
|
|
144
|
+
if (st === undefined && value !== "default") return config;
|
|
145
|
+
return Object.freeze({ ...config, subSampling: Object.freeze({ ...config.subSampling, temperature: st }) });
|
|
146
|
+
}
|
|
108
147
|
case "sandboxInitTimeoutMs": return Object.freeze({ ...config, sandboxInitTimeoutMs: Number(value) });
|
|
109
148
|
case "requestTimeoutMs": return Object.freeze({ ...config, requestTimeoutMs: Number(value) * 60_000 });
|
|
110
149
|
case "contextLoader": return Object.freeze({ ...config, contextLoader: value === "on" });
|
|
@@ -29,6 +29,8 @@ export interface RunRegistration {
|
|
|
29
29
|
readonly rootModel?: () => string | undefined;
|
|
30
30
|
/** Root's OWN spend (driver-model turns) — never a subtree sum across models. */
|
|
31
31
|
readonly rootTokens?: () => number;
|
|
32
|
+
readonly rootTokensIn?: () => number;
|
|
33
|
+
readonly rootTokensOut?: () => number;
|
|
32
34
|
/** Persistent entries (background work) stay hidden until they hold subcalls. */
|
|
33
35
|
readonly hideWhenEmpty?: boolean;
|
|
34
36
|
}
|
|
@@ -44,6 +46,8 @@ export interface RunEntry {
|
|
|
44
46
|
readonly turns: () => { readonly current: number; readonly max: number };
|
|
45
47
|
readonly rootModel: () => string | undefined;
|
|
46
48
|
readonly rootTokens: () => number;
|
|
49
|
+
readonly rootTokensIn: () => number;
|
|
50
|
+
readonly rootTokensOut: () => number;
|
|
47
51
|
readonly hideWhenEmpty: boolean;
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -66,6 +70,8 @@ export class RunRegistry {
|
|
|
66
70
|
turns: run.turns ?? (() => DEFAULT_TURNS),
|
|
67
71
|
rootModel: run.rootModel ?? (() => undefined),
|
|
68
72
|
rootTokens: run.rootTokens ?? (() => 0),
|
|
73
|
+
rootTokensIn: run.rootTokensIn ?? (() => 0),
|
|
74
|
+
rootTokensOut: run.rootTokensOut ?? (() => 0),
|
|
69
75
|
hideWhenEmpty: run.hideWhenEmpty ?? false,
|
|
70
76
|
};
|
|
71
77
|
this.entries.set(run.runId, entry);
|
|
@@ -119,6 +125,8 @@ export class RunRegistry {
|
|
|
119
125
|
rootPhase: entry.rootPhase(),
|
|
120
126
|
rootModel: entry.rootModel(),
|
|
121
127
|
rootTokens: entry.rootTokens(),
|
|
128
|
+
rootTokensIn: entry.rootTokensIn(),
|
|
129
|
+
rootTokensOut: entry.rootTokensOut(),
|
|
122
130
|
subcalls: entry.subcalls(),
|
|
123
131
|
});
|
|
124
132
|
}
|
package/src/ui/theme.ts
CHANGED
|
@@ -12,6 +12,12 @@ export function formatTokens(n: number): string {
|
|
|
12
12
|
return String(n);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** `190.2k↑ 18.6k↓ tok` — the in/out split. Falls back to plain when out is zero. */
|
|
16
|
+
export function formatTokensSplit(tokensIn: number, tokensOut: number): string {
|
|
17
|
+
if (tokensOut > 0) return `${formatTokens(tokensIn)}↑ ${formatTokens(tokensOut)}↓ tok`;
|
|
18
|
+
return `${formatTokens(tokensIn)} tok`;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
export function formatDuration(ms: number): string {
|
|
16
22
|
const s = ms / 1000;
|
|
17
23
|
return s < 60 ? `${s.toFixed(1)}s` : `${Math.floor(s / 60)}m${Math.round(s % 60)}s`;
|
|
@@ -26,6 +26,8 @@ export interface RunSnapshot {
|
|
|
26
26
|
readonly rootModel?: string;
|
|
27
27
|
/** Root's OWN token spend (driver-model turns) — never a subtree sum. */
|
|
28
28
|
readonly rootTokens: number;
|
|
29
|
+
readonly rootTokensIn: number;
|
|
30
|
+
readonly rootTokensOut: number;
|
|
29
31
|
readonly subcalls: readonly RlmSubcall[];
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -46,6 +48,8 @@ export interface NodeRow {
|
|
|
46
48
|
readonly label: string;
|
|
47
49
|
/** The row's OWN token spend for its OWN model — never a subtree sum. */
|
|
48
50
|
readonly tokens: number;
|
|
51
|
+
readonly tokensIn: number;
|
|
52
|
+
readonly tokensOut: number;
|
|
49
53
|
readonly model?: string;
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -64,6 +68,8 @@ export interface GroupRow {
|
|
|
64
68
|
readonly model?: string;
|
|
65
69
|
/** Sum over members — one model only (the group key pins it), so never a blend. */
|
|
66
70
|
readonly tokens: number;
|
|
71
|
+
readonly tokensIn: number;
|
|
72
|
+
readonly tokensOut: number;
|
|
67
73
|
/** SubcallStatus, or "queued" while any member parks on the rate-limit cooldown. */
|
|
68
74
|
readonly icon: SubcallStatus | "queued";
|
|
69
75
|
readonly expandable: boolean;
|
|
@@ -147,6 +153,8 @@ export function buildRows(
|
|
|
147
153
|
phase: sc.phase,
|
|
148
154
|
label: sc.label,
|
|
149
155
|
tokens: sc.tokens,
|
|
156
|
+
tokensIn: sc.tokensIn,
|
|
157
|
+
tokensOut: sc.tokensOut,
|
|
150
158
|
model: sc.model,
|
|
151
159
|
});
|
|
152
160
|
if (!expanded || children.length === 0) return;
|
|
@@ -181,7 +189,13 @@ export function buildRows(
|
|
|
181
189
|
const id = `grp:${run.runId}:${entry.key}:${first.id}`;
|
|
182
190
|
const expanded = expandedGroups.has(id);
|
|
183
191
|
let tokens = 0;
|
|
184
|
-
|
|
192
|
+
let tokensIn = 0;
|
|
193
|
+
let tokensOut = 0;
|
|
194
|
+
for (const m of entry.members) {
|
|
195
|
+
tokens += m.tokens;
|
|
196
|
+
tokensIn += m.tokensIn;
|
|
197
|
+
tokensOut += m.tokensOut;
|
|
198
|
+
}
|
|
185
199
|
rows.push({
|
|
186
200
|
type: "group",
|
|
187
201
|
id,
|
|
@@ -192,6 +206,8 @@ export function buildRows(
|
|
|
192
206
|
label: entry.label,
|
|
193
207
|
model: entry.model,
|
|
194
208
|
tokens,
|
|
209
|
+
tokensIn,
|
|
210
|
+
tokensOut,
|
|
195
211
|
icon: iconOf(entry.status, entry.members.some((m) => m.phase === "queued") ? "queued" : undefined),
|
|
196
212
|
expandable: true,
|
|
197
213
|
expanded,
|
|
@@ -218,6 +234,8 @@ export function buildRows(
|
|
|
218
234
|
phase: run.rootPhase,
|
|
219
235
|
label: run.rootLabel,
|
|
220
236
|
tokens: run.rootTokens,
|
|
237
|
+
tokensIn: run.rootTokensIn,
|
|
238
|
+
tokensOut: run.rootTokensOut,
|
|
221
239
|
model: run.rootModel,
|
|
222
240
|
});
|
|
223
241
|
if (!collapsed.has(run.runId)) visitChildren(roots, 0, "");
|
package/src/ui/tree/tree-rows.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
|
-
import { formatTokens, spinnerFrame } from "../theme.ts";
|
|
11
|
+
import { formatTokens, formatTokensSplit, spinnerFrame } from "../theme.ts";
|
|
12
12
|
import type { GroupRow, NodeRow, TreeRow } from "./tree-model.ts";
|
|
13
13
|
|
|
14
14
|
const GLYPHS = Object.freeze({ done: "✓", error: "✗", queued: "◷", expanded: "▾", collapsed: "▸", leaf: " " } as const);
|
|
@@ -29,16 +29,16 @@ function iconGlyph(row: NodeRow, theme: Theme): string {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Right-hand stats: "
|
|
33
|
-
function statsText(tokens: number, model: string | undefined): string {
|
|
34
|
-
const parts = [`${formatTokens(tokens)} tok`];
|
|
32
|
+
/** Right-hand stats: "190.2k↑ 18.6k↓ tok · gpt-5-mini" (split + model only when present). */
|
|
33
|
+
function statsText(tokens: number, tokensIn: number, tokensOut: number, model: string | undefined): string {
|
|
34
|
+
const parts = [tokensOut > 0 ? formatTokensSplit(tokensIn, tokensOut) : `${formatTokens(tokens)} tok`];
|
|
35
35
|
if (model !== undefined) parts.push(modelShort(model));
|
|
36
36
|
return parts.join(" · ");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** Left/right assembly shared by node and group rows — one padding rule, no fork. */
|
|
40
|
-
function assembleLine(left: string, tokens: number, model: string | undefined, selected: boolean, width: number, theme: Theme): string {
|
|
41
|
-
const right = theme.fg("dim", statsText(tokens, model));
|
|
40
|
+
function assembleLine(left: string, tokens: number, tokensIn: number, tokensOut: number, model: string | undefined, selected: boolean, width: number, theme: Theme): string {
|
|
41
|
+
const right = theme.fg("dim", statsText(tokens, tokensIn, tokensOut, model));
|
|
42
42
|
const gap = width - visibleWidth(left) - visibleWidth(right) - 1;
|
|
43
43
|
const line = gap > 0 ? `${left}${" ".repeat(gap)}${right}` : `${truncateToWidth(left, width - 1)} `;
|
|
44
44
|
return selected ? theme.fg("accent", line) : line;
|
|
@@ -48,7 +48,7 @@ function formatNode(row: NodeRow, selected: boolean, width: number, theme: Theme
|
|
|
48
48
|
const chevron = row.expandable ? (row.expanded ? GLYPHS.expanded : GLYPHS.collapsed) : GLYPHS.leaf;
|
|
49
49
|
const cursor = selected ? theme.fg("accent", "❯") : " ";
|
|
50
50
|
const left = `${cursor} ${row.prefix}${chevron} ${iconGlyph(row, theme)} ${row.label}`;
|
|
51
|
-
return assembleLine(left, row.tokens, row.model, selected, width, theme);
|
|
51
|
+
return assembleLine(left, row.tokens, row.tokensIn, row.tokensOut, row.model, selected, width, theme);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function formatGroup(row: GroupRow, selected: boolean, width: number, theme: Theme): string {
|
|
@@ -56,7 +56,7 @@ function formatGroup(row: GroupRow, selected: boolean, width: number, theme: The
|
|
|
56
56
|
const cursor = selected ? theme.fg("accent", "❯") : " ";
|
|
57
57
|
const icon = row.icon === "done" ? theme.fg("success", GLYPHS.done) : row.icon === "error" ? theme.fg("error", GLYPHS.error) : theme.fg("warning", spinnerFrame());
|
|
58
58
|
const left = `${cursor} ${row.prefix}${chevron} ${icon} ${row.label} ×${row.count}`;
|
|
59
|
-
return assembleLine(left, row.tokens, row.model, selected, width, theme);
|
|
59
|
+
return assembleLine(left, row.tokens, row.tokensIn, row.tokensOut, row.model, selected, width, theme);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function formatRow(row: TreeRow, selected: boolean, width: number, theme: Theme): string {
|
package/src/util/retry.ts
CHANGED
|
@@ -17,10 +17,13 @@ import { ProviderCooldown, sleepMs, sharedCooldown } from "./throttle.ts";
|
|
|
17
17
|
|
|
18
18
|
// Auth/quota failures must not be retried — they burn attempts and never recover.
|
|
19
19
|
const NON_RETRYABLE_TEXT =
|
|
20
|
-
/api[ -]?key|unauthorized|forbidden|permission denied|billing|insufficient|balance|quota exceeded|not.?found|context length|too large|invalid request|malformed/i;
|
|
21
|
-
// Transport/server transients — worth another attempt.
|
|
20
|
+
/api[ -]?key|unauthorized|forbidden|permission denied|billing|insufficient|balance|quota exceeded|not.?found|context length|too large|invalid request|malformed|content.?filter/i;
|
|
21
|
+
// Transport/server transients — worth another attempt. "Provider finish_reason: error" is the
|
|
22
|
+
// generic shape OpenRouter relays when the UPSTREAM kills a generation mid-stream (observed
|
|
23
|
+
// from Cohere's free pool: native_finish_reason "error", no message, no code, partial usage) —
|
|
24
|
+
// HTTP 200, so it is inherently transient-by-nature and must be retried.
|
|
22
25
|
const RETRYABLE_TEXT =
|
|
23
|
-
/\b429\b|rate.?limit|overloaded|service.?unavailable|upstream|timeout|timed.?out|temporarily|try.?again|econnreset|econnrefused|etimedout|socket hang up|network|1302|速率|频率/i;
|
|
26
|
+
/\b429\b|rate.?limit|overloaded|service.?unavailable|upstream|timeout|timed.?out|temporarily|try.?again|econnreset|econnrefused|etimedout|socket hang up|network|finish.?reason: ?(error|network_error)|1302|速率|频率/i;
|
|
24
27
|
const RATE_LIMIT_TEXT = /\b429\b|rate.?limit|1302|速率|频率/i;
|
|
25
28
|
|
|
26
29
|
const NON_RETRYABLE_STATUS = new Set([400, 401, 402, 403, 404, 413, 422]);
|