@danypops/pi-jittor 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/docs/USAGE_PRIOR_ART.md +1 -1
- package/extension/src/index.ts +173 -43
- package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +200 -27
- package/extension/src/observability/context-growth.ts +26 -0
- package/extension/src/{context-report.ts → observability/context-report.ts} +35 -3
- package/extension/src/observability/context-view.ts +264 -0
- package/extension/src/{footer.ts → observability/footer.ts} +7 -0
- package/extension/src/observability/provider-context-snapshot.ts +246 -0
- package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +1 -1
- package/extension/src/{tui.ts → observability/status.ts} +55 -10
- package/extension/src/{usage.ts → observability/usage.ts} +74 -166
- package/extension/src/{benchmark-tui.ts → optimization/model-selection-panel.ts} +61 -29
- package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +2 -2
- package/extension/src/service-client.ts +48 -1
- package/extension/src/settings-tui.ts +57 -32
- package/extension/src/settings.ts +27 -19
- package/package.json +9 -3
- package/extension/src/context-view.ts +0 -140
- /package/extension/src/{capabilities → observability}/context-hub.ts +0 -0
- /package/extension/src/{capabilities → observability}/http-headers.ts +0 -0
- /package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +0 -0
|
@@ -62,6 +62,11 @@ export type ProviderBudget =
|
|
|
62
62
|
label: string;
|
|
63
63
|
valueText: string;
|
|
64
64
|
observedAt?: number;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
kind: "unavailable";
|
|
68
|
+
label: string;
|
|
69
|
+
valueText: string;
|
|
65
70
|
};
|
|
66
71
|
|
|
67
72
|
export interface CompactionProgress {
|
|
@@ -221,9 +226,11 @@ function budgetSegment(
|
|
|
221
226
|
if (budget === undefined) return undefined;
|
|
222
227
|
const w = barWidth(width);
|
|
223
228
|
if (!budget) return `budget ${theme.fg("dim", progressBar(null, w))} ?`;
|
|
229
|
+
if (budget.kind === "unavailable") return `${budget.label} ${theme.fg("warning", budget.valueText)}`;
|
|
224
230
|
const stale = budget.observedAt !== undefined && now - budget.observedAt > TELEMETRY_STALE_AFTER_MS;
|
|
225
231
|
const staleText = stale ? ` ${theme.fg("warning", "stale")}` : "";
|
|
226
232
|
if (budget.kind === "unbounded") return `${budget.label} ${budget.valueText}${staleText}`;
|
|
233
|
+
if (budget.resetsAt !== undefined && budget.resetsAt <= now) return `${budget.label} ${theme.fg("warning", "reset pending")}`;
|
|
227
234
|
const remaining = Math.min(1, Math.max(0, budget.remainingFraction));
|
|
228
235
|
const bar = theme.fg(fillColor(1 - remaining), progressBar(remaining, w));
|
|
229
236
|
const value = `${compact ? Math.round(remaining * 100) : (remaining * 100).toFixed(1)}% left`;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTEXT_SNAPSHOT_MAX_SEGMENTS,
|
|
3
|
+
type ContextFingerprinter,
|
|
4
|
+
type ContextSegmentSource,
|
|
5
|
+
type ContextSnapshot,
|
|
6
|
+
countTextWithFallback,
|
|
7
|
+
StructuralTextTokenCounter,
|
|
8
|
+
type TextTokenCounter,
|
|
9
|
+
validateContextSnapshot,
|
|
10
|
+
} from "@danypops/jittor";
|
|
11
|
+
|
|
12
|
+
const CANONICAL_MAX_DEPTH = 16;
|
|
13
|
+
const CANONICAL_MAX_NODES = 10_000;
|
|
14
|
+
const CANONICAL_MAX_STRING_CHARACTERS = 65_536;
|
|
15
|
+
const CANONICAL_MAX_COLLECTION_ITEMS = 256;
|
|
16
|
+
|
|
17
|
+
export interface ProviderContextHistoryEntry {
|
|
18
|
+
id: string;
|
|
19
|
+
type: string;
|
|
20
|
+
message?: unknown;
|
|
21
|
+
summary?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProviderContextHistoryNode {
|
|
25
|
+
entry: ProviderContextHistoryEntry;
|
|
26
|
+
children: ProviderContextHistoryNode[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProviderContextHistory {
|
|
30
|
+
roots: readonly ProviderContextHistoryNode[];
|
|
31
|
+
/** Pi buildContextEntries(): compaction-aware entries that contribute to the request now. */
|
|
32
|
+
activeEntryIds: ReadonlySet<string>;
|
|
33
|
+
/** Pi getBranch(): raw current branch, including entries summarized away by compaction. */
|
|
34
|
+
branchEntryIds: ReadonlySet<string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProviderContextSnapshotInput {
|
|
38
|
+
/** Final provider payload. Ephemeral: this function returns only keyed fingerprints and sizes. */
|
|
39
|
+
payload: unknown;
|
|
40
|
+
captureId: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
provider: string;
|
|
43
|
+
model: string;
|
|
44
|
+
capturedAt: number;
|
|
45
|
+
fingerprinter: ContextFingerprinter;
|
|
46
|
+
counters?: readonly TextTokenCounter[];
|
|
47
|
+
history?: ProviderContextHistory;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CanonicalValue {
|
|
51
|
+
text: string;
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CanonicalState {
|
|
56
|
+
nodes: number;
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
seen: WeakSet<object>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function boundedString(value: string, state: CanonicalState): string {
|
|
62
|
+
if (value.length <= CANONICAL_MAX_STRING_CHARACTERS) return JSON.stringify(value);
|
|
63
|
+
state.truncated = true;
|
|
64
|
+
const half = Math.floor(CANONICAL_MAX_STRING_CHARACTERS / 2);
|
|
65
|
+
return JSON.stringify(`${value.slice(0, half)}…[${value.length} chars]…${value.slice(-half)}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function canonical(value: unknown, depth: number, state: CanonicalState): string {
|
|
69
|
+
state.nodes += 1;
|
|
70
|
+
if (state.nodes > CANONICAL_MAX_NODES || depth > CANONICAL_MAX_DEPTH) {
|
|
71
|
+
state.truncated = true;
|
|
72
|
+
return '"[bounded]"';
|
|
73
|
+
}
|
|
74
|
+
if (value === null) return "null";
|
|
75
|
+
if (typeof value === "string") return boundedString(value, state);
|
|
76
|
+
if (typeof value === "number") return Number.isFinite(value) ? JSON.stringify(value) : '"[non-finite]"';
|
|
77
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
78
|
+
if (typeof value === "bigint") return JSON.stringify(`${value.toString()}n`);
|
|
79
|
+
if (typeof value !== "object") return JSON.stringify(`[${typeof value}]`);
|
|
80
|
+
if (state.seen.has(value)) {
|
|
81
|
+
state.truncated = true;
|
|
82
|
+
return '"[cycle]"';
|
|
83
|
+
}
|
|
84
|
+
state.seen.add(value);
|
|
85
|
+
try {
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
if (value.length > CANONICAL_MAX_COLLECTION_ITEMS) state.truncated = true;
|
|
88
|
+
const shown = value.slice(0, CANONICAL_MAX_COLLECTION_ITEMS).map((item) => canonical(item, depth + 1, state));
|
|
89
|
+
if (value.length > shown.length) shown.push(JSON.stringify(`[${value.length - shown.length} omitted]`));
|
|
90
|
+
return `[${shown.join(",")}]`;
|
|
91
|
+
}
|
|
92
|
+
const record = value as Record<string, unknown>;
|
|
93
|
+
const keys = Object.keys(record).sort();
|
|
94
|
+
if (keys.length > CANONICAL_MAX_COLLECTION_ITEMS) state.truncated = true;
|
|
95
|
+
const shown = keys
|
|
96
|
+
.slice(0, CANONICAL_MAX_COLLECTION_ITEMS)
|
|
97
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(record[key], depth + 1, state)}`);
|
|
98
|
+
if (keys.length > shown.length) shown.push(`${JSON.stringify("[omitted]")}:${keys.length - shown.length}`);
|
|
99
|
+
return `{${shown.join(",")}}`;
|
|
100
|
+
} finally {
|
|
101
|
+
state.seen.delete(value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function boundedCanonical(value: unknown): CanonicalValue {
|
|
106
|
+
const state: CanonicalState = { nodes: 0, truncated: false, seen: new WeakSet() };
|
|
107
|
+
return { text: canonical(value, 0, state), truncated: state.truncated };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function objectRecord(value: unknown): Record<string, unknown> | null {
|
|
111
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function blockSource(block: unknown, role: unknown): ContextSegmentSource {
|
|
115
|
+
if (role === "tool" || role === "function") return "tool-result";
|
|
116
|
+
const record = objectRecord(block);
|
|
117
|
+
const type = typeof record?.type === "string" ? record.type.toLowerCase() : "";
|
|
118
|
+
if (type.includes("thinking") || type.includes("reasoning")) return "thinking";
|
|
119
|
+
if (type.includes("tool_result") || type.includes("tool-result") || type.includes("function_call_output")) return "tool-result";
|
|
120
|
+
if (type.includes("tool_use") || type.includes("tool-use") || type.includes("function_call")) return "tool-call";
|
|
121
|
+
if (role === "system" || role === "developer") return "base-prompt";
|
|
122
|
+
return "conversation-message";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function imageLike(value: unknown): boolean {
|
|
126
|
+
const record = objectRecord(value);
|
|
127
|
+
if (!record || typeof record.type !== "string") return false;
|
|
128
|
+
return record.type.toLowerCase().includes("image");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extracts an ordered, provider-neutral structural view from common chat/responses payload shapes.
|
|
133
|
+
* Unknown shapes remain one conversation segment; the payload is never returned or retained.
|
|
134
|
+
*/
|
|
135
|
+
export function captureProviderContextSnapshot(input: ProviderContextSnapshotInput): ContextSnapshot {
|
|
136
|
+
const segments: ContextSnapshot["segments"] = [];
|
|
137
|
+
let truncated = false;
|
|
138
|
+
const fallback = new StructuralTextTokenCounter();
|
|
139
|
+
const push = (
|
|
140
|
+
value: unknown,
|
|
141
|
+
source: ContextSegmentSource,
|
|
142
|
+
logicalPath: string,
|
|
143
|
+
state: ContextSnapshot["segments"][number]["state"] = "active",
|
|
144
|
+
): void => {
|
|
145
|
+
if (segments.length >= CONTEXT_SNAPSHOT_MAX_SEGMENTS) {
|
|
146
|
+
truncated = true;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const encoded = boundedCanonical(value);
|
|
150
|
+
truncated ||= encoded.truncated;
|
|
151
|
+
const measurement = imageLike(value)
|
|
152
|
+
? null
|
|
153
|
+
: countTextWithFallback(
|
|
154
|
+
{ text: encoded.text, scope: "context-item", provider: input.provider, model: input.model },
|
|
155
|
+
input.counters ?? [],
|
|
156
|
+
fallback,
|
|
157
|
+
);
|
|
158
|
+
segments.push({
|
|
159
|
+
id: input.fingerprinter.fingerprint(`segment:${source}:${logicalPath}`),
|
|
160
|
+
fingerprint: input.fingerprinter.fingerprint(`value:${encoded.text}`),
|
|
161
|
+
source,
|
|
162
|
+
tokens: measurement?.tokens ?? 0,
|
|
163
|
+
state,
|
|
164
|
+
requestPosition: state === "active" ? segments.filter((segment) => segment.requestPosition !== null).length : null,
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const payload = objectRecord(input.payload);
|
|
169
|
+
if (!payload) {
|
|
170
|
+
push(input.payload, "conversation-message", "payload");
|
|
171
|
+
} else {
|
|
172
|
+
for (const key of ["instructions", "system", "system_instruction"] as const) {
|
|
173
|
+
if (payload[key] !== undefined) push(payload[key], "base-prompt", key);
|
|
174
|
+
}
|
|
175
|
+
const tools = Array.isArray(payload.tools) ? payload.tools : Array.isArray(payload.functions) ? payload.functions : [];
|
|
176
|
+
for (let index = 0; index < tools.length; index++) push(tools[index], "tool-definitions", `tools/${index}`);
|
|
177
|
+
|
|
178
|
+
const conversation = Array.isArray(payload.messages)
|
|
179
|
+
? payload.messages
|
|
180
|
+
: Array.isArray(payload.input)
|
|
181
|
+
? payload.input
|
|
182
|
+
: Array.isArray(payload.contents)
|
|
183
|
+
? payload.contents
|
|
184
|
+
: payload.input === undefined
|
|
185
|
+
? []
|
|
186
|
+
: [payload.input];
|
|
187
|
+
for (let messageIndex = 0; messageIndex < conversation.length; messageIndex++) {
|
|
188
|
+
const message = conversation[messageIndex];
|
|
189
|
+
const record = objectRecord(message);
|
|
190
|
+
const role = record?.role;
|
|
191
|
+
const content = record?.content ?? record?.parts;
|
|
192
|
+
if (Array.isArray(content)) {
|
|
193
|
+
for (let blockIndex = 0; blockIndex < content.length; blockIndex++) {
|
|
194
|
+
push(content[blockIndex], blockSource(content[blockIndex], role), `messages/${messageIndex}/content/${blockIndex}`);
|
|
195
|
+
}
|
|
196
|
+
} else if (content !== undefined) {
|
|
197
|
+
push(content, blockSource(message, role), `messages/${messageIndex}/content`);
|
|
198
|
+
} else {
|
|
199
|
+
push(message, blockSource(message, role), `messages/${messageIndex}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (input.history) {
|
|
205
|
+
const visited = new Set<string>();
|
|
206
|
+
const stack = [...input.history.roots].reverse();
|
|
207
|
+
while (stack.length > 0) {
|
|
208
|
+
if (segments.length >= CONTEXT_SNAPSHOT_MAX_SEGMENTS) {
|
|
209
|
+
truncated = true;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
const node = stack.pop()!;
|
|
213
|
+
if (visited.has(node.entry.id)) {
|
|
214
|
+
truncated = true;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
visited.add(node.entry.id);
|
|
218
|
+
stack.push(...[...node.children].reverse());
|
|
219
|
+
if (input.history.activeEntryIds.has(node.entry.id)) continue;
|
|
220
|
+
const state = input.history.branchEntryIds.has(node.entry.id) ? "compacted" : "inactive";
|
|
221
|
+
const message = objectRecord(node.entry.message);
|
|
222
|
+
const role = message?.role;
|
|
223
|
+
const content = message?.content ?? message?.parts;
|
|
224
|
+
if (Array.isArray(content)) {
|
|
225
|
+
for (let index = 0; index < content.length; index++)
|
|
226
|
+
push(content[index], blockSource(content[index], role), `history/${node.entry.id}/${index}`, state);
|
|
227
|
+
} else if (content !== undefined) {
|
|
228
|
+
push(content, blockSource(node.entry.message, role), `history/${node.entry.id}`, state);
|
|
229
|
+
} else if (node.entry.summary !== undefined) {
|
|
230
|
+
push(node.entry.summary, "conversation-message", `history/${node.entry.id}/summary`, state);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (stack.length > 0) truncated = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return validateContextSnapshot({
|
|
237
|
+
version: 1,
|
|
238
|
+
snapshotId: input.fingerprinter.fingerprint(`snapshot:${input.sessionId}:${input.captureId}`),
|
|
239
|
+
sessionId: input.fingerprinter.fingerprint(`session:${input.sessionId}`),
|
|
240
|
+
provider: input.provider,
|
|
241
|
+
model: input.model,
|
|
242
|
+
capturedAt: input.capturedAt,
|
|
243
|
+
truncated,
|
|
244
|
+
segments,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
parseAnthropicRateLimitHeaders,
|
|
8
8
|
parseCodexRateLimitHeaders,
|
|
9
9
|
} from "@danypops/jittor";
|
|
10
|
-
import { headerValue } from "
|
|
10
|
+
import { headerValue } from "../observability/http-headers.ts";
|
|
11
11
|
|
|
12
12
|
export interface ProviderTelemetryClient {
|
|
13
13
|
call(operation: string, input: unknown): Promise<any>;
|
|
@@ -6,11 +6,12 @@ import {
|
|
|
6
6
|
type Route,
|
|
7
7
|
type RouterStatus,
|
|
8
8
|
type StoredMetricObservation,
|
|
9
|
+
TELEMETRY_STALE_AFTER_MS,
|
|
9
10
|
} from "@danypops/jittor";
|
|
10
11
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
13
|
+
import { sessionSecretField } from "../session-identity.ts";
|
|
12
14
|
import type { ProviderBudget } from "./footer.ts";
|
|
13
|
-
import { sessionSecretField } from "./session-identity.ts";
|
|
14
15
|
|
|
15
16
|
export interface JittorPanelClient {
|
|
16
17
|
call(operation: string, input: unknown): Promise<any>;
|
|
@@ -104,20 +105,56 @@ function windowName(seconds: number): string {
|
|
|
104
105
|
* could ever read -- see google-vertex-contracts.ts); the footer omits the segment entirely rather
|
|
105
106
|
* than showing a `?` that can never resolve.
|
|
106
107
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
type CodexTelemetryState = "available" | "missing" | "failed" | "stale";
|
|
109
|
+
|
|
110
|
+
function codexTelemetryState(status: RouterStatus, now: number): CodexTelemetryState {
|
|
111
|
+
const source = status.sources.find((candidate) => candidate.id === "codex-subscription");
|
|
112
|
+
if (!source) return "missing";
|
|
113
|
+
if (!source.ok) return "failed";
|
|
114
|
+
if (source.observedAt !== undefined && now - source.observedAt > TELEMETRY_STALE_AFTER_MS) return "stale";
|
|
115
|
+
return "available";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function unavailableCodexBudget(
|
|
119
|
+
label: string,
|
|
120
|
+
reason: "reset pending" | "telemetry unavailable" | "telemetry failed" | "telemetry stale",
|
|
121
|
+
): ProviderBudget {
|
|
122
|
+
return { kind: "unavailable", label, valueText: reason };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildFooterBudget(
|
|
126
|
+
status: RouterStatus,
|
|
127
|
+
metrics: StoredMetricObservation[],
|
|
128
|
+
now = Date.now(),
|
|
129
|
+
): ProviderBudget | null | undefined {
|
|
130
|
+
if (!status.currentRoute) return null;
|
|
109
131
|
if (status.currentRoute.provider === "openai-codex") {
|
|
110
132
|
const codex = codexWindowForModel(metrics, status.currentRoute.model);
|
|
111
|
-
|
|
133
|
+
const sourceState = codexTelemetryState(status, now);
|
|
134
|
+
if (!codex || typeof codex.value !== "number") {
|
|
135
|
+
if (sourceState === "missing") return unavailableCodexBudget("Codex", "telemetry unavailable");
|
|
136
|
+
if (sourceState === "failed") return unavailableCodexBudget("Codex", "telemetry failed");
|
|
137
|
+
if (sourceState === "stale") return unavailableCodexBudget("Codex", "telemetry stale");
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const label = compactWindowName(Number(codex.attributes.windowSeconds ?? 0));
|
|
112
141
|
const resetsAtSeconds = Number(codex.attributes.resetsAt);
|
|
142
|
+
const resetsAt = Number.isFinite(resetsAtSeconds) && resetsAtSeconds > 0 ? resetsAtSeconds * 1_000 : undefined;
|
|
143
|
+
if (resetsAt !== undefined && resetsAt <= now) return unavailableCodexBudget(label, "reset pending");
|
|
144
|
+
if (now - codex.observedAt > TELEMETRY_STALE_AFTER_MS) {
|
|
145
|
+
if (sourceState === "missing") return unavailableCodexBudget(label, "telemetry unavailable");
|
|
146
|
+
if (sourceState === "failed") return unavailableCodexBudget(label, "telemetry failed");
|
|
147
|
+
return unavailableCodexBudget(label, "telemetry stale");
|
|
148
|
+
}
|
|
113
149
|
return {
|
|
114
150
|
kind: "bounded",
|
|
115
|
-
label
|
|
151
|
+
label,
|
|
116
152
|
remainingFraction: 1 - codex.value,
|
|
117
153
|
observedAt: codex.observedAt,
|
|
118
|
-
...(
|
|
154
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
119
155
|
};
|
|
120
156
|
}
|
|
157
|
+
if (!status.ready) return null;
|
|
121
158
|
if (status.currentRoute.provider === "anthropic") {
|
|
122
159
|
const anthropic =
|
|
123
160
|
latest(
|
|
@@ -188,10 +225,12 @@ export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObs
|
|
|
188
225
|
return undefined;
|
|
189
226
|
}
|
|
190
227
|
|
|
191
|
-
export function formatFooterStatus(status: RouterStatus, metrics: StoredMetricObservation[]): string {
|
|
192
|
-
const budget = buildFooterBudget(status, metrics);
|
|
228
|
+
export function formatFooterStatus(status: RouterStatus, metrics: StoredMetricObservation[], now = Date.now()): string {
|
|
229
|
+
const budget = buildFooterBudget(status, metrics, now);
|
|
193
230
|
if (!budget) return "";
|
|
194
|
-
|
|
231
|
+
if (budget.kind === "unbounded") return budget.valueText;
|
|
232
|
+
if (budget.kind === "unavailable") return `${budget.label} ${budget.valueText}`;
|
|
233
|
+
return `${budget.label} ${(budget.remainingFraction * 100).toFixed(1)}% left`;
|
|
195
234
|
}
|
|
196
235
|
|
|
197
236
|
function nextAction(action: PolicyAction | undefined): string {
|
|
@@ -238,10 +277,14 @@ function burnLine(rows: StoredMetricObservation[], current: StoredMetricObservat
|
|
|
238
277
|
export function buildStatusView(status: RouterStatus, metrics: StoredMetricObservation[], now = Date.now()): string[] {
|
|
239
278
|
const lines = [status.ready ? "Ready" : "Not ready"];
|
|
240
279
|
const codex = status.currentRoute?.provider === "openai-codex" ? codexWindowForModel(metrics, status.currentRoute.model) : undefined;
|
|
241
|
-
|
|
280
|
+
const budget = buildFooterBudget(status, metrics, now);
|
|
281
|
+
if (codex && typeof codex.value === "number" && budget?.kind === "bounded") {
|
|
242
282
|
const seconds = Number(codex.attributes.windowSeconds ?? 0);
|
|
243
283
|
lines.push(`Codex ${windowName(seconds)}: ${((1 - codex.value) * 100).toFixed(1)}% left`);
|
|
244
284
|
lines.push(burnLine(metrics, codex, now));
|
|
285
|
+
} else if (status.currentRoute?.provider === "openai-codex" && budget?.kind === "unavailable") {
|
|
286
|
+
const seconds = Number(codex?.attributes.windowSeconds ?? 0);
|
|
287
|
+
lines.push(`Codex ${codex ? windowName(seconds) : "subscription"}: ${budget.valueText}`);
|
|
245
288
|
}
|
|
246
289
|
const openRouter =
|
|
247
290
|
status.currentRoute?.provider === "openrouter"
|
|
@@ -270,6 +313,8 @@ export function buildStatusView(status: RouterStatus, metrics: StoredMetricObser
|
|
|
270
313
|
lines.push(`Next: ${nextAction(status.lastDecision?.action)}`);
|
|
271
314
|
lines.push("Telemetry:");
|
|
272
315
|
const providerSources = status.sources.filter((source) => source.provider === status.currentRoute?.provider);
|
|
316
|
+
if (status.currentRoute?.provider === "openai-codex" && providerSources.length === 0)
|
|
317
|
+
lines.push(" codex-subscription: unavailable · not configured by active daemon");
|
|
273
318
|
for (const source of providerSources.slice(0, HUMAN_STATUS_MAX_SOURCES)) {
|
|
274
319
|
const freshness = !source.ok ? "failed" : source.observedAt !== undefined && now - source.observedAt > 120_000 ? "stale" : "fresh";
|
|
275
320
|
lines.push(` ${sanitizedText(source.id)}: ${freshness} · ${source.metrics} metrics`);
|