@centerforagenticai/pi-multi-account 0.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/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- package/src/window-history.ts +270 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import type { ManagedFamily } from "./config.js";
|
|
2
|
+
import type { ProviderType } from "./vendor.js";
|
|
3
|
+
import { formatDuration } from "./duration.js";
|
|
4
|
+
import type { UnsupportedModelPair } from "./model-support.js";
|
|
5
|
+
import { GAP_WINDOW_ID, type UsageFetchStatus } from "./usage-fetch.js";
|
|
6
|
+
import type { UsageQuotaProjection, UsageSnapshot } from "./usage.js";
|
|
7
|
+
import type { WindowSample } from "./window-history.js";
|
|
8
|
+
import {
|
|
9
|
+
computeCostAggregate,
|
|
10
|
+
getLatestWindowSamples,
|
|
11
|
+
projectUsageQuota,
|
|
12
|
+
} from "./usage.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Human-facing renderer for `/multi-account status`.
|
|
16
|
+
*
|
|
17
|
+
* The previous renderer emitted raw JSON, which forced an operator to read
|
|
18
|
+
* `coolingUntilMs` epochs and `"usage": undefined` holes. This module owns
|
|
19
|
+
* presentation only: it never reads credentials, never performs I/O, and never
|
|
20
|
+
* mutates routing state.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Display labels for the managed families, in stable display order. */
|
|
24
|
+
const FAMILY_LABELS: Readonly<Record<ManagedFamily, string>> = Object.freeze({
|
|
25
|
+
anthropic: "Anthropic (Claude)",
|
|
26
|
+
"openai-codex": "OpenAI Codex (GPT)",
|
|
27
|
+
"google-antigravity": "Google Antigravity",
|
|
28
|
+
openai: "OpenAI (API)",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const FAMILY_ORDER: readonly ManagedFamily[] = Object.freeze([
|
|
32
|
+
"anthropic",
|
|
33
|
+
"openai-codex",
|
|
34
|
+
"google-antigravity",
|
|
35
|
+
"openai",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/** Human-facing labels for the live provider type of a managed account. */
|
|
39
|
+
const PROVIDER_TYPE_LABELS: Readonly<Record<ProviderType, string>> =
|
|
40
|
+
Object.freeze({
|
|
41
|
+
subscription: "subscription",
|
|
42
|
+
"owning-vendor-api": "API key",
|
|
43
|
+
openrouter: "OpenRouter",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** Renders a bounded provider-type suffix, or an empty string when unknown. */
|
|
47
|
+
function providerTypeSuffix(providerType: ProviderType | undefined): string {
|
|
48
|
+
return providerType === undefined
|
|
49
|
+
? ""
|
|
50
|
+
: ` — ${PROVIDER_TYPE_LABELS[providerType]}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Fraction of a limit at or below which an account is "approaching" it. */
|
|
54
|
+
const LOW_HEADROOM_FRACTION = 0.15;
|
|
55
|
+
|
|
56
|
+
/** A single account's presentation-ready state. */
|
|
57
|
+
export interface AccountStatusView {
|
|
58
|
+
readonly providerId: string;
|
|
59
|
+
readonly family: ManagedFamily;
|
|
60
|
+
/**
|
|
61
|
+
* How this account reaches its models, derived live from the discovered
|
|
62
|
+
* credential type. Presentation-only; never a routing input in this child.
|
|
63
|
+
*/
|
|
64
|
+
readonly providerType?: ProviderType;
|
|
65
|
+
readonly active: boolean;
|
|
66
|
+
readonly disabled: boolean;
|
|
67
|
+
readonly unavailable: boolean;
|
|
68
|
+
readonly coolingUntilMs?: number;
|
|
69
|
+
/** Recent repeated 429s prove the provider's usage reading is not actionable. */
|
|
70
|
+
readonly usageUntrusted?: boolean;
|
|
71
|
+
readonly usage?: UsageSnapshot;
|
|
72
|
+
/** Bounded state of detached authoritative usage fetching for this account. */
|
|
73
|
+
readonly usageFetch?: UsageFetchStatus;
|
|
74
|
+
/** Model id in use, shown only for the active account. */
|
|
75
|
+
readonly activeModelId?: string;
|
|
76
|
+
/** True when every model advertised by this account was rejected this session. */
|
|
77
|
+
readonly allModelsUnsupported?: boolean;
|
|
78
|
+
/** Operator-facing account label, when one is configured or derived. */
|
|
79
|
+
readonly label?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Bounded credential expiry. Reported even for accounts idle this session,
|
|
82
|
+
* because Pi's lazy refresh means an unused credential silently ages out.
|
|
83
|
+
*/
|
|
84
|
+
readonly expiresAtMs?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Derived, non-reversible identity of the underlying account. Two slots
|
|
87
|
+
* sharing one means the operator has the same account logged in twice.
|
|
88
|
+
*
|
|
89
|
+
* Never a credential value: derived at the auth boundary and only the derived
|
|
90
|
+
* string travels. Undefined for opaque-token families, which is why an
|
|
91
|
+
* Anthropic duplicate cannot be detected this way and a Codex one can.
|
|
92
|
+
*/
|
|
93
|
+
readonly accountFingerprint?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface StatusViewInput {
|
|
97
|
+
readonly accounts: readonly AccountStatusView[];
|
|
98
|
+
readonly nowMs: number;
|
|
99
|
+
readonly declarationNotice?: {
|
|
100
|
+
readonly condition: "stale";
|
|
101
|
+
readonly remedy: string;
|
|
102
|
+
readonly status: "mismatched" | "unreadable";
|
|
103
|
+
};
|
|
104
|
+
/** True when the caller scoped output to a single account. */
|
|
105
|
+
readonly scoped?: boolean;
|
|
106
|
+
/** Session-local account/model pairs that the provider reported unavailable. */
|
|
107
|
+
readonly unsupportedModels?: readonly UnsupportedModelPair[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Groups slots whose derived account identity matches.
|
|
112
|
+
*
|
|
113
|
+
* Works on the DERIVED fingerprint the status input already carries, never on a
|
|
114
|
+
* credential value. A slot with no fingerprint is never grouped, so an
|
|
115
|
+
* opaque-token family yields no false positives rather than guessing.
|
|
116
|
+
*/
|
|
117
|
+
function duplicateSlotGroups(
|
|
118
|
+
accounts: readonly AccountStatusView[],
|
|
119
|
+
): ReadonlyArray<readonly string[]> {
|
|
120
|
+
const byFingerprint = new Map<string, string[]>();
|
|
121
|
+
for (const account of accounts) {
|
|
122
|
+
const fingerprint = account.accountFingerprint;
|
|
123
|
+
if (fingerprint === undefined || fingerprint.length === 0) continue;
|
|
124
|
+
const group = byFingerprint.get(fingerprint);
|
|
125
|
+
if (group) group.push(account.providerId);
|
|
126
|
+
else byFingerprint.set(fingerprint, [account.providerId]);
|
|
127
|
+
}
|
|
128
|
+
return [...byFingerprint.values()].filter((group) => group.length > 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Coarse health, ordered most-severe first. */
|
|
132
|
+
export type AccountHealth =
|
|
133
|
+
| "unavailable"
|
|
134
|
+
| "disabled"
|
|
135
|
+
| "cooling"
|
|
136
|
+
| "rate-limited"
|
|
137
|
+
| "exhausted"
|
|
138
|
+
| "low-headroom"
|
|
139
|
+
| "ready";
|
|
140
|
+
|
|
141
|
+
function formatCount(value: number): string {
|
|
142
|
+
if (value < 1_000) return String(value);
|
|
143
|
+
if (value < 1_000_000) {
|
|
144
|
+
const thousands = value / 1_000;
|
|
145
|
+
return `${thousands >= 100 ? Math.round(thousands) : thousands.toFixed(1)}k`;
|
|
146
|
+
}
|
|
147
|
+
const millions = value / 1_000_000;
|
|
148
|
+
return `${millions >= 100 ? Math.round(millions) : millions.toFixed(1)}M`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* True when a server-reported remaining budget has fallen into the bottom
|
|
153
|
+
* {@link LOW_HEADROOM_FRACTION} of the largest value observed this session.
|
|
154
|
+
*
|
|
155
|
+
* Providers report REMAINING counts but not the ceiling, so "approaching the
|
|
156
|
+
* limit" is inferred against the peak seen for that account. This is an
|
|
157
|
+
* observation-derived hint, never an authoritative quota reading.
|
|
158
|
+
*/
|
|
159
|
+
function isLowHeadroom(
|
|
160
|
+
usage: UsageSnapshot | undefined,
|
|
161
|
+
quota: UsageQuotaProjection,
|
|
162
|
+
): boolean {
|
|
163
|
+
if (!usage || quota.status !== "fresh") return false;
|
|
164
|
+
const { remainingRequests, remainingTokens } = quota;
|
|
165
|
+
const { observedPeakRequests, observedPeakTokens } = usage;
|
|
166
|
+
const lowRequests =
|
|
167
|
+
remainingRequests !== undefined &&
|
|
168
|
+
observedPeakRequests !== undefined &&
|
|
169
|
+
observedPeakRequests > 0 &&
|
|
170
|
+
remainingRequests / observedPeakRequests <= LOW_HEADROOM_FRACTION;
|
|
171
|
+
const lowTokens =
|
|
172
|
+
remainingTokens !== undefined &&
|
|
173
|
+
observedPeakTokens !== undefined &&
|
|
174
|
+
observedPeakTokens > 0 &&
|
|
175
|
+
remainingTokens / observedPeakTokens <= LOW_HEADROOM_FRACTION;
|
|
176
|
+
return lowRequests || lowTokens;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function accountHealth(
|
|
180
|
+
account: AccountStatusView,
|
|
181
|
+
nowMs: number,
|
|
182
|
+
): AccountHealth {
|
|
183
|
+
if (account.unavailable) return "unavailable";
|
|
184
|
+
if (account.disabled) return "disabled";
|
|
185
|
+
if (account.coolingUntilMs !== undefined && account.coolingUntilMs > nowMs) {
|
|
186
|
+
return "cooling";
|
|
187
|
+
}
|
|
188
|
+
if (account.usageUntrusted) return "rate-limited";
|
|
189
|
+
const quota = projectUsageQuota(account.usage, nowMs);
|
|
190
|
+
if (
|
|
191
|
+
quota.status === "fresh" &&
|
|
192
|
+
(quota.remainingRequests === 0 ||
|
|
193
|
+
quota.remainingTokens === 0 ||
|
|
194
|
+
(quota.utilization !== undefined && quota.utilization >= 1))
|
|
195
|
+
) {
|
|
196
|
+
return "exhausted";
|
|
197
|
+
}
|
|
198
|
+
if (isLowHeadroom(account.usage, quota)) return "low-headroom";
|
|
199
|
+
return "ready";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function healthLabel(
|
|
203
|
+
health: AccountHealth,
|
|
204
|
+
account: AccountStatusView,
|
|
205
|
+
nowMs: number,
|
|
206
|
+
): string {
|
|
207
|
+
switch (health) {
|
|
208
|
+
case "unavailable":
|
|
209
|
+
return "unavailable (re-auth needed)";
|
|
210
|
+
case "disabled":
|
|
211
|
+
return "disabled by operator";
|
|
212
|
+
case "cooling":
|
|
213
|
+
return `cooling down, ~${formatDuration((account.coolingUntilMs ?? nowMs) - nowMs)} left`;
|
|
214
|
+
case "rate-limited":
|
|
215
|
+
return "rate limited; recent 429s override the usage reading";
|
|
216
|
+
case "exhausted":
|
|
217
|
+
return "exhausted";
|
|
218
|
+
case "low-headroom":
|
|
219
|
+
return "ready, but approaching its limit";
|
|
220
|
+
case "ready":
|
|
221
|
+
return "ready";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Describes credential freshness. Unlike limit data this is available for every
|
|
227
|
+
* account, used or not, so an idle account still reports something actionable.
|
|
228
|
+
*/
|
|
229
|
+
function credentialLine(
|
|
230
|
+
account: AccountStatusView,
|
|
231
|
+
nowMs: number,
|
|
232
|
+
): string | undefined {
|
|
233
|
+
if (account.expiresAtMs === undefined) return undefined;
|
|
234
|
+
const remaining = account.expiresAtMs - nowMs;
|
|
235
|
+
if (remaining <= 0) return "credential EXPIRED — sign in again";
|
|
236
|
+
return `credential expires in ${formatDuration(remaining)}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Renders what the provider actually told us about remaining budget. Returns
|
|
241
|
+
* undefined when nothing is known about this account's budget.
|
|
242
|
+
*
|
|
243
|
+
* `utilization` leads because it is the only figure that answers "can I use
|
|
244
|
+
* this account" directly. Providers report what is LEFT rather than a ceiling,
|
|
245
|
+
* so remaining counts appear only when no percentage is available — a bare
|
|
246
|
+
* "400k tokens left" means little without the budget it came from.
|
|
247
|
+
*/
|
|
248
|
+
function limitLine(
|
|
249
|
+
account: AccountStatusView,
|
|
250
|
+
nowMs: number,
|
|
251
|
+
): string | undefined {
|
|
252
|
+
const usage = projectUsageQuota(account.usage, nowMs);
|
|
253
|
+
if (usage.status !== "fresh") return undefined;
|
|
254
|
+
const parts: string[] = [];
|
|
255
|
+
if (usage.utilization !== undefined) {
|
|
256
|
+
parts.push(`${Math.round(usage.utilization * 100)}% used`);
|
|
257
|
+
} else {
|
|
258
|
+
if (usage.remainingRequests !== undefined) {
|
|
259
|
+
parts.push(`${formatCount(usage.remainingRequests)} requests left`);
|
|
260
|
+
}
|
|
261
|
+
if (usage.remainingTokens !== undefined) {
|
|
262
|
+
parts.push(`${formatCount(usage.remainingTokens)} tokens left`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
usage.recoveryAtMs !== undefined &&
|
|
267
|
+
Number.isFinite(usage.recoveryAtMs) &&
|
|
268
|
+
usage.recoveryAtMs > nowMs
|
|
269
|
+
) {
|
|
270
|
+
parts.push(`resets in ${formatDuration(usage.recoveryAtMs - nowMs)}`);
|
|
271
|
+
}
|
|
272
|
+
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Age beyond which a reading is worth flagging as possibly out of date. */
|
|
276
|
+
const STALE_READING_MS = 10 * 60 * 1000;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A single caveat line, emitted ONLY when something is genuinely off.
|
|
280
|
+
*
|
|
281
|
+
* Routine provenance — which endpoint answered, how fresh a current reading is,
|
|
282
|
+
* that fetching is working — is noise that crowds out the figure an operator
|
|
283
|
+
* came to read. It earns a line only when the number should not be taken at
|
|
284
|
+
* face value: stale, fetching stopped, or observed by a peer rather than here.
|
|
285
|
+
*/
|
|
286
|
+
function usageCaveatLine(
|
|
287
|
+
account: AccountStatusView,
|
|
288
|
+
nowMs: number,
|
|
289
|
+
): string | undefined {
|
|
290
|
+
const fetch = account.usageFetch;
|
|
291
|
+
if (fetch && !fetch.enabled) {
|
|
292
|
+
return "usage fetching disabled by config — figures may be incomplete";
|
|
293
|
+
}
|
|
294
|
+
if (fetch?.disabled) {
|
|
295
|
+
return `usage fetching stopped (${fetch.disabledReason ?? "repeated failures"}) — showing retained data`;
|
|
296
|
+
}
|
|
297
|
+
const usage = account.usage;
|
|
298
|
+
if (!usage) return undefined;
|
|
299
|
+
const quota = projectUsageQuota(usage, nowMs);
|
|
300
|
+
if (quota.status === "stale") {
|
|
301
|
+
return "usage observation is stale — retained quota is not used for health";
|
|
302
|
+
}
|
|
303
|
+
const ageMs =
|
|
304
|
+
usage.observationScope === "fleet"
|
|
305
|
+
? (usage.ageMs ?? 0)
|
|
306
|
+
: Math.max(0, nowMs - usage.snapshotAtMs);
|
|
307
|
+
if (ageMs > STALE_READING_MS) {
|
|
308
|
+
return `reading is ${ageLabel(ageMs)} — may be out of date`;
|
|
309
|
+
}
|
|
310
|
+
if (usage.observationScope === "fleet") {
|
|
311
|
+
return `observed by ${usage.observerId ?? "another agent"} (${ageLabel(ageMs)})`;
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function ageLabel(ageMs: number): string {
|
|
317
|
+
return `${formatDuration(ageMs)} ago`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function usedLine(usage: UsageSnapshot | undefined): string | undefined {
|
|
321
|
+
if (!usage) return undefined;
|
|
322
|
+
const total = usage.inputTokens + usage.outputTokens;
|
|
323
|
+
if (total === 0) return undefined;
|
|
324
|
+
if (usage.observationScope === "fleet") {
|
|
325
|
+
return `fleet-observed usage: ${formatCount(total)} tokens (${formatCount(usage.inputTokens)} in / ${formatCount(usage.outputTokens)} out)`;
|
|
326
|
+
}
|
|
327
|
+
return `used this session: ${formatCount(total)} tokens (${formatCount(usage.inputTokens)} in / ${formatCount(usage.outputTokens)} out)`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* REQ-WINDOW-RENDER: Render one window as percentage remaining with reset time.
|
|
332
|
+
* Shape: "5h 73% left / 2h14m"
|
|
333
|
+
* REQ-NO-IDENTIFYING-DATA: windowId is included but must not be a human-identifying label.
|
|
334
|
+
*/
|
|
335
|
+
function renderWindow(sample: WindowSample, nowMs: number): string {
|
|
336
|
+
const parts: string[] = [];
|
|
337
|
+
|
|
338
|
+
// Window identifier (safe - this is a provider-supplied window ID, not user data)
|
|
339
|
+
parts.push(`[${sample.windowId}]`);
|
|
340
|
+
|
|
341
|
+
// Percentage remaining
|
|
342
|
+
if (sample.remainingFraction !== undefined) {
|
|
343
|
+
const percentRemaining = Math.round(sample.remainingFraction * 100);
|
|
344
|
+
parts.push(`${percentRemaining}% left`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Reset time
|
|
348
|
+
if (sample.resetAtMs !== undefined && sample.resetAtMs > nowMs) {
|
|
349
|
+
parts.push(`resets in ${formatDuration(sample.resetAtMs - nowMs)}`);
|
|
350
|
+
} else if (sample.resetAtMs !== undefined && sample.resetAtMs <= nowMs) {
|
|
351
|
+
parts.push("reset overdue");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return parts.join(" · ");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* REQ-WINDOW-RENDER: Render all windows for an account, one line per window.
|
|
359
|
+
* Returns undefined if no window data is available.
|
|
360
|
+
*/
|
|
361
|
+
function windowsLine(
|
|
362
|
+
providerId: string,
|
|
363
|
+
nowMs: number,
|
|
364
|
+
latestSamples: ReadonlyMap<string, readonly WindowSample[]>,
|
|
365
|
+
): string | undefined {
|
|
366
|
+
const accountSamples = latestSamples.get(providerId) ?? [];
|
|
367
|
+
if (accountSamples.length === 0) return undefined;
|
|
368
|
+
|
|
369
|
+
const lines: string[] = [];
|
|
370
|
+
for (const sample of accountSamples) {
|
|
371
|
+
const { windowId } = sample;
|
|
372
|
+
// `__gap__` is an internal sentinel (usage-fetch.ts:503) marking a stretch
|
|
373
|
+
// of time the fetcher could not cover. It is bookkeeping, not a provider
|
|
374
|
+
// window, and rendering it verbatim put a double-underscore token in front
|
|
375
|
+
// of the operator that reads like broken markup and names nothing they can
|
|
376
|
+
// act on. Suppressed here rather than at the source, because the record is
|
|
377
|
+
// load-bearing for coverage accounting.
|
|
378
|
+
if (windowId === GAP_WINDOW_ID) continue;
|
|
379
|
+
if (sample.usable) {
|
|
380
|
+
lines.push(renderWindow(sample, nowMs));
|
|
381
|
+
} else {
|
|
382
|
+
lines.push(`[${windowId}] ${sample.unusableReason ?? "unusable"}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return lines.length > 0 ? `windows: ${lines.join(", ")}` : undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* REQ-COVERAGE-STATE: Render cost aggregate with coverage label.
|
|
391
|
+
* REQ-COST-INCOMPLETE: NO unqualified total when coverage is not complete.
|
|
392
|
+
*/
|
|
393
|
+
function costAggregateLine(
|
|
394
|
+
periodStartMs: number,
|
|
395
|
+
periodEndMs: number,
|
|
396
|
+
): string | undefined {
|
|
397
|
+
const aggregate = computeCostAggregate(periodStartMs, periodEndMs);
|
|
398
|
+
|
|
399
|
+
const parts: string[] = [];
|
|
400
|
+
|
|
401
|
+
// Coverage state always leads
|
|
402
|
+
parts.push(`coverage: ${aggregate.coverageState}`);
|
|
403
|
+
|
|
404
|
+
// REQ-COVERAGE-STATE: NO unqualified total unless coverage is complete
|
|
405
|
+
if (aggregate.coverageState === "complete") {
|
|
406
|
+
parts.push(`total: $${aggregate.completeTotal.toFixed(4)}`);
|
|
407
|
+
} else {
|
|
408
|
+
// Partial/unknown: show what we have with explicit qualification
|
|
409
|
+
parts.push(`complete records: $${aggregate.completeTotal.toFixed(4)}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// REQ-COST-INCOMPLETE: surface partial and unpriced counts separately
|
|
413
|
+
if (aggregate.partialCount > 0) {
|
|
414
|
+
parts.push(`partial: ${aggregate.partialCount}`);
|
|
415
|
+
}
|
|
416
|
+
if (aggregate.unpricedCount > 0) {
|
|
417
|
+
parts.push(`unpriced: ${aggregate.unpricedCount}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return parts.join(" · ");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function renderAccount(
|
|
424
|
+
account: AccountStatusView,
|
|
425
|
+
nowMs: number,
|
|
426
|
+
indent: string,
|
|
427
|
+
latestWindowSamples: ReadonlyMap<string, readonly WindowSample[]>,
|
|
428
|
+
): string[] {
|
|
429
|
+
const health = accountHealth(account, nowMs);
|
|
430
|
+
const marker =
|
|
431
|
+
health === "ready" ? "•" : health === "low-headroom" ? "!" : "×";
|
|
432
|
+
const named = account.label
|
|
433
|
+
? `${account.providerId} (${account.label})`
|
|
434
|
+
: account.providerId;
|
|
435
|
+
const typeTag =
|
|
436
|
+
account.providerType === undefined
|
|
437
|
+
? ""
|
|
438
|
+
: ` [${PROVIDER_TYPE_LABELS[account.providerType]}]`;
|
|
439
|
+
const lines = [
|
|
440
|
+
`${indent}${marker} ${named}${typeTag} — ${healthLabel(health, account, nowMs)}`,
|
|
441
|
+
];
|
|
442
|
+
const credential = credentialLine(account, nowMs);
|
|
443
|
+
if (credential) lines.push(`${indent} ${credential}`);
|
|
444
|
+
const limits = limitLine(account, nowMs);
|
|
445
|
+
if (limits) lines.push(`${indent} ${limits}`);
|
|
446
|
+
const windows = windowsLine(account.providerId, nowMs, latestWindowSamples);
|
|
447
|
+
if (windows) lines.push(`${indent} ${windows}`);
|
|
448
|
+
const used = usedLine(account.usage);
|
|
449
|
+
if (used) lines.push(`${indent} ${used}`);
|
|
450
|
+
const caveat = usageCaveatLine(account, nowMs);
|
|
451
|
+
if (caveat) lines.push(`${indent} ${caveat}`);
|
|
452
|
+
if (account.allModelsUnsupported) {
|
|
453
|
+
lines.push(
|
|
454
|
+
`${indent} no advertised models available — every model was rejected this session`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (!account.usage) {
|
|
458
|
+
lines.push(`${indent} no usage data yet for this account`);
|
|
459
|
+
}
|
|
460
|
+
return lines;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Renders the operator-facing status report: the active account first with its
|
|
465
|
+
* limit posture, then every other account grouped by family.
|
|
466
|
+
*/
|
|
467
|
+
export function renderStatus(input: StatusViewInput): string {
|
|
468
|
+
const { accounts, nowMs } = input;
|
|
469
|
+
const declarationBanner =
|
|
470
|
+
input.declarationNotice === undefined
|
|
471
|
+
? undefined
|
|
472
|
+
: input.declarationNotice.status === "mismatched"
|
|
473
|
+
? `Managed model declaration is stale; logical routing is using the live catalog. Run ${input.declarationNotice.remedy} to re-sync it.`
|
|
474
|
+
: `LOGICAL ROUTING OFF: The managed model declaration is unreadable. Run ${input.declarationNotice.remedy}.`;
|
|
475
|
+
if (accounts.length === 0) {
|
|
476
|
+
const message =
|
|
477
|
+
"No managed accounts are available. Run /multi-account rediscover.";
|
|
478
|
+
return declarationBanner === undefined
|
|
479
|
+
? message
|
|
480
|
+
: `${declarationBanner}\n${message}`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const active = accounts.find((account) => account.active);
|
|
484
|
+
const others = accounts.filter((account) => !account.active);
|
|
485
|
+
const sections: string[] =
|
|
486
|
+
declarationBanner === undefined ? [] : [declarationBanner];
|
|
487
|
+
|
|
488
|
+
if (active) {
|
|
489
|
+
const health = accountHealth(active, nowMs);
|
|
490
|
+
const header = [
|
|
491
|
+
"ACTIVE ACCOUNT",
|
|
492
|
+
` ${active.providerId} [${FAMILY_LABELS[active.family]}${providerTypeSuffix(active.providerType)}]`,
|
|
493
|
+
...(active.label ? [` account: ${active.label}`] : []),
|
|
494
|
+
...(active.activeModelId ? [` model: ${active.activeModelId}`] : []),
|
|
495
|
+
` status: ${healthLabel(health, active, nowMs)}`,
|
|
496
|
+
];
|
|
497
|
+
const credential = credentialLine(active, nowMs);
|
|
498
|
+
if (credential) header.push(` ${credential}`);
|
|
499
|
+
const limits = limitLine(active, nowMs);
|
|
500
|
+
header.push(limits ? ` limits: ${limits}` : " limits: no usage data yet");
|
|
501
|
+
const used = usedLine(active.usage);
|
|
502
|
+
if (used) header.push(` ${used}`);
|
|
503
|
+
const caveat = usageCaveatLine(active, nowMs);
|
|
504
|
+
if (caveat) header.push(` ${caveat}`);
|
|
505
|
+
if (active.allModelsUnsupported) {
|
|
506
|
+
header.push(
|
|
507
|
+
" no advertised models available — every model was rejected this session",
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
sections.push(header.join("\n"));
|
|
511
|
+
} else {
|
|
512
|
+
sections.push(
|
|
513
|
+
"ACTIVE ACCOUNT\n none — the current model is not a managed account.",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (others.length > 0) {
|
|
518
|
+
const latestWindowSamples = getLatestWindowSamples();
|
|
519
|
+
const ready = others.filter(
|
|
520
|
+
(account) => accountHealth(account, nowMs) === "ready",
|
|
521
|
+
).length;
|
|
522
|
+
const grouped: string[] = [
|
|
523
|
+
`OTHER ACCOUNTS (${ready} of ${others.length} ready)`,
|
|
524
|
+
];
|
|
525
|
+
for (const family of FAMILY_ORDER) {
|
|
526
|
+
const members = others.filter((account) => account.family === family);
|
|
527
|
+
if (members.length === 0) continue;
|
|
528
|
+
grouped.push(` ${FAMILY_LABELS[family]}`);
|
|
529
|
+
for (const member of members) {
|
|
530
|
+
grouped.push(
|
|
531
|
+
...renderAccount(member, nowMs, " ", latestWindowSamples),
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
sections.push(grouped.join("\n"));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// REQ-COVERAGE-STATE: Add cost aggregate section
|
|
539
|
+
// Use last 24 hours as the period
|
|
540
|
+
const periodStartMs = nowMs - 24 * 60 * 60 * 1000;
|
|
541
|
+
const costLine = costAggregateLine(periodStartMs, nowMs);
|
|
542
|
+
if (costLine) {
|
|
543
|
+
sections.push(`COST AGGREGATE (last 24 hours)\n ${costLine}`);
|
|
544
|
+
}
|
|
545
|
+
sections.push(
|
|
546
|
+
"Other agents on this machine share these accounts. Figures fetched from\n" +
|
|
547
|
+
"the provider include their consumption; figures derived from this\n" +
|
|
548
|
+
"session's own responses do not.",
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const unsupported = input.unsupportedModels ?? [];
|
|
552
|
+
if (unsupported.length > 0) {
|
|
553
|
+
const lines = ["UNSUPPORTED ACCOUNT/MODEL PAIRS THIS SESSION"];
|
|
554
|
+
for (const pair of unsupported) {
|
|
555
|
+
lines.push(` ${pair.providerId} cannot serve ${pair.modelId}`);
|
|
556
|
+
}
|
|
557
|
+
sections.push(lines.join("\n"));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Two slots backed by one account halve the capacity the operator believes
|
|
561
|
+
// they have: failover routes from a rate-limited slot to its twin, which is
|
|
562
|
+
// the same exhausted account, and burns a turn discovering that. Nothing
|
|
563
|
+
// reported this before, so it failed silently.
|
|
564
|
+
const duplicates = duplicateSlotGroups(input.accounts);
|
|
565
|
+
if (duplicates.length > 0) {
|
|
566
|
+
const lines = ["DUPLICATE ACCOUNTS"];
|
|
567
|
+
for (const group of duplicates) {
|
|
568
|
+
lines.push(` same account: ${group.join(", ")}`);
|
|
569
|
+
}
|
|
570
|
+
lines.push(
|
|
571
|
+
" These slots share one account, so they share its rate limit.",
|
|
572
|
+
" Failover between them cannot help; log one in to a different account.",
|
|
573
|
+
);
|
|
574
|
+
sections.push(lines.join("\n"));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return sections.join("\n\n");
|
|
578
|
+
}
|