@songsid/agend 2.3.4-beta.7 → 2.3.4-beta.9
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/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +3 -1
- package/dist/agent-endpoint.js +13 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +28 -4
- package/dist/authority/session-authorizations.js +63 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- package/dist/backend/codex.js +8 -3
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +39 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -1
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-ledger.d.ts +85 -0
- package/dist/cache-ledger.js +236 -1
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +11 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +4 -3
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +43 -0
- package/dist/daemon.js +361 -17
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +23 -1
- package/dist/delivery-consumed-watch.js +30 -3
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +7 -0
- package/dist/delivery-reconciliation.js +9 -1
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +85 -4
- package/dist/fleet-manager.js +375 -30
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/concepts/SKILL.md +390 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/instance-lifecycle.d.ts +8 -2
- package/dist/instance-lifecycle.js +37 -6
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +11 -0
- package/dist/kiro-engine-status.js +6 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/locale.js +47 -1
- package/dist/locale.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/login-flows.d.ts +2 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/needs-you-hub.js +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +6 -0
- package/dist/needs-you.js +8 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.js +3 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.js +5 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.js.map +1 -1
- package/dist/settings-api.d.ts +13 -0
- package/dist/settings-api.js +12 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +6 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/panel-chat.js +2 -0
- package/dist/ui/panel-needs.js +4 -3
- package/dist/ui/settings-strings.js +2 -2
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-needs.js +1 -1
- package/dist/ui/shared/app.css +15 -0
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/view-strings.js +16 -0
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/usage-api.d.ts +31 -0
- package/dist/usage/usage-api.js +80 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +4 -1
- package/dist/usage-rollup.js +12 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +6 -0
- package/dist/web-api.js +8 -2
- package/dist/web-api.js.map +1 -1
- package/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* Keep this list explicit: the parity test rejects a key missing from either
|
|
4
4
|
* locale surface before a mixed-language release can ship.
|
|
5
5
|
*/
|
|
6
|
-
export declare const USAGE_I18N_KEYS: readonly ["usage.title", "usage.title_plain", "usage.empty_active", "usage.not_logged_in", "usage.default_login", "usage.error_fallback", "usage.no_data", "usage.reset.in", "usage.reset.soon", "usage.duration.days_hours", "usage.duration.hours_minutes", "usage.duration.minutes", "usage.metric.session", "usage.metric.weekly", "usage.metric.monthly", "usage.metric.days_window", "usage.metric.named", "usage.metric.named_session", "usage.metric.named_weekly", "usage.metric.named_monthly", "usage.metric.named_days", "usage.metric.extra_usage", "usage.metric.rate_limit_resets", "usage.ticket_expiry", "usage.metric.credits", "usage.metric.weekly_limit", "usage.metric.pay_as_you_go", "usage.metric.bonus_credits", "usage.metric.overage_charges", "usage.metric.pay_per_use", "usage.unit.available", "usage.unit.credits", "usage.metric.usage_monthly", "usage.note.used_limit_unit", "usage.note.bonus_codes", "usage.value.disabled", "usage.value.cap", "usage.value.unlimited", "usage.note.binding", "usage.note.binding_severity", "usage.note.busiest_model", "usage.note.busiest_models", "usage.metric.agy_claude_others", "usage.metric.agy_claude_others_session", "usage.metric.agy_claude_others_weekly", "usage.hint.statusline_now", "usage.hint.statusline_minutes", "usage.hint.claude_api_key", "usage.hint.login_claude", "usage.hint.login_agy", "usage.hint.no_quota", "usage.hint.login_codex", "usage.hint.codex_no_oauth", "usage.hint.login_grok", "usage.hint.grok_no_login", "usage.hint.login_kiro", "usage.hint.kiro_signed_out", "usage.hint.claude_profile_signed_out", "usage.hint.claude_profile_signed_out_chat", "usage.hint.token_refreshing", "usage.hint.subscription_no_credit", "usage.hint.no_quota_account", "usage.error.unreachable", "usage.error.token_rejected", "usage.error.rate_limited", "usage.error.rate_limited_transient", "usage.stale_rate_limited", "usage.stale.expired", "usage.stale.transient", "usage.stale.error", "usage.metric.reset_since", "usage.error.http", "usage.error.billing_http", "usage.error.invalid_response", "usage.error.invalid_billing_response", "usage.error.invalid_kiro_response", "usage.error.claude_token_expired", "usage.error.codex_token_expired", "usage.error.grok_expired", "usage.error.grok_response_changed", "usage.error.agy_cap", "usage.error.agy_no_refresh", "usage.error.agy_token_expired", "usage.error.agy_login_expired", "usage.error.agy_refresh_empty", "usage.error.grok_auth_readonly", "usage.error.grok_no_refresh", "usage.error.grok_refresh_unreachable", "usage.error.grok_invalid_refresh", "usage.error.grok_persist"];
|
|
6
|
+
export declare const USAGE_I18N_KEYS: readonly ["usage.title", "usage.title_plain", "usage.empty_active", "usage.not_logged_in", "usage.default_login", "usage.error_fallback", "usage.no_data", "usage.reset.in", "usage.reset.soon", "usage.duration.days_hours", "usage.duration.hours_minutes", "usage.duration.minutes", "usage.metric.session", "usage.metric.weekly", "usage.metric.monthly", "usage.metric.days_window", "usage.metric.named", "usage.metric.named_session", "usage.metric.named_weekly", "usage.metric.named_monthly", "usage.metric.named_days", "usage.metric.extra_usage", "usage.metric.rate_limit_resets", "usage.ticket_expiry", "usage.metric.credits", "usage.metric.weekly_limit", "usage.metric.pay_as_you_go", "usage.metric.bonus_credits", "usage.metric.overage_charges", "usage.metric.pay_per_use", "usage.unit.available", "usage.unit.credits", "usage.metric.usage_monthly", "usage.note.used_limit_unit", "usage.note.bonus_codes", "usage.value.disabled", "usage.value.cap", "usage.value.unlimited", "usage.note.binding", "usage.note.binding_severity", "usage.note.busiest_model", "usage.note.busiest_models", "usage.metric.agy_claude_others", "usage.metric.agy_claude_others_session", "usage.metric.agy_claude_others_weekly", "usage.hint.statusline_now", "usage.hint.statusline_minutes", "usage.hint.claude_api_key", "usage.hint.login_claude", "usage.hint.login_agy", "usage.hint.no_quota", "usage.hint.login_codex", "usage.hint.codex_no_oauth", "usage.hint.login_grok", "usage.hint.grok_no_login", "usage.hint.login_kiro", "usage.hint.kiro_signed_out", "usage.hint.claude_profile_signed_out", "usage.hint.claude_profile_signed_out_chat", "usage.hint.token_refreshing", "usage.hint.subscription_no_credit", "usage.hint.no_quota_account", "usage.error.unreachable", "usage.error.token_rejected", "usage.error.rate_limited", "usage.error.rate_limited_transient", "usage.stale_rate_limited", "usage.stale.expired", "usage.stale.transient", "usage.stale.error", "usage.metric.reset_since", "usage.error.http", "usage.error.billing_http", "usage.error.invalid_response", "usage.error.invalid_billing_response", "usage.error.invalid_kiro_response", "usage.error.claude_token_expired", "usage.error.codex_token_expired", "usage.error.grok_expired", "usage.error.grok_response_changed", "usage.error.agy_cap", "usage.error.agy_no_refresh", "usage.error.agy_token_expired", "usage.error.agy_login_expired", "usage.error.agy_refresh_empty", "usage.error.grok_auth_readonly", "usage.error.grok_no_refresh", "usage.error.grok_refresh_unreachable", "usage.error.grok_invalid_refresh", "usage.error.grok_persist", "usage.capacity.line", "usage.capacity.line_tokens", "usage.capacity.not_enough", "usage.capacity.only_agend", "usage.capacity.mixed", "usage.capacity.best_fit_api", "usage.capacity.best_fit_output", "usage.capacity.best_fit_all"];
|
|
7
7
|
export type UsageI18nKey = typeof USAGE_I18N_KEYS[number];
|
package/dist/usage/i18n-keys.js
CHANGED
|
@@ -31,5 +31,8 @@ export const USAGE_I18N_KEYS = [
|
|
|
31
31
|
"usage.error.agy_no_refresh", "usage.error.agy_token_expired", "usage.error.agy_login_expired",
|
|
32
32
|
"usage.error.agy_refresh_empty", "usage.error.grok_auth_readonly", "usage.error.grok_no_refresh",
|
|
33
33
|
"usage.error.grok_refresh_unreachable", "usage.error.grok_invalid_refresh", "usage.error.grok_persist",
|
|
34
|
+
// #3066 §1: a weekly quota's measured 100%.
|
|
35
|
+
"usage.capacity.line", "usage.capacity.line_tokens", "usage.capacity.not_enough", "usage.capacity.only_agend", "usage.capacity.mixed",
|
|
36
|
+
"usage.capacity.best_fit_api", "usage.capacity.best_fit_output", "usage.capacity.best_fit_all",
|
|
34
37
|
];
|
|
35
38
|
//# sourceMappingURL=i18n-keys.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n-keys.js","sourceRoot":"","sources":["../../src/usage/i18n-keys.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,aAAa,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,qBAAqB;IACtG,sBAAsB,EAAE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB;IAC7E,2BAA2B,EAAE,8BAA8B,EAAE,wBAAwB;IACrF,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,0BAA0B;IACjG,oBAAoB,EAAE,4BAA4B,EAAE,2BAA2B;IAC/E,4BAA4B,EAAE,yBAAyB,EAAE,0BAA0B;IACnF,gCAAgC,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,2BAA2B;IAC5G,4BAA4B,EAAE,4BAA4B,EAAE,8BAA8B;IAC1F,0BAA0B,EAAE,sBAAsB,EAAE,oBAAoB;IACxE,4BAA4B,EAAE,4BAA4B,EAAE,wBAAwB;IACpF,sBAAsB,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,oBAAoB;IACxF,6BAA6B,EAAE,0BAA0B,EAAE,2BAA2B;IACtF,gCAAgC,EAAE,wCAAwC,EAAE,uCAAuC;IACnH,2BAA2B,EAAE,+BAA+B,EAAE,2BAA2B;IACzF,yBAAyB,EAAE,sBAAsB,EAAE,qBAAqB;IACxE,wBAAwB,EAAE,2BAA2B,EAAE,uBAAuB;IAC9E,0BAA0B,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,sCAAsC,EAAE,2CAA2C;IACtK,6BAA6B,EAAE,mCAAmC,EAAE,6BAA6B;IACjG,yBAAyB,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,oCAAoC,EAAE,0BAA0B;IACrJ,qBAAqB,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,0BAA0B;IAC/F,kBAAkB,EAAE,0BAA0B,EAAE,8BAA8B;IAC9E,sCAAsC,EAAE,mCAAmC;IAC3E,kCAAkC,EAAE,iCAAiC;IACrE,0BAA0B,EAAE,mCAAmC,EAAE,qBAAqB;IACtF,4BAA4B,EAAE,+BAA+B,EAAE,+BAA+B;IAC9F,+BAA+B,EAAE,gCAAgC,EAAE,6BAA6B;IAChG,sCAAsC,EAAE,kCAAkC,EAAE,0BAA0B;
|
|
1
|
+
{"version":3,"file":"i18n-keys.js","sourceRoot":"","sources":["../../src/usage/i18n-keys.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,aAAa,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,qBAAqB;IACtG,sBAAsB,EAAE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB;IAC7E,2BAA2B,EAAE,8BAA8B,EAAE,wBAAwB;IACrF,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,0BAA0B;IACjG,oBAAoB,EAAE,4BAA4B,EAAE,2BAA2B;IAC/E,4BAA4B,EAAE,yBAAyB,EAAE,0BAA0B;IACnF,gCAAgC,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,2BAA2B;IAC5G,4BAA4B,EAAE,4BAA4B,EAAE,8BAA8B;IAC1F,0BAA0B,EAAE,sBAAsB,EAAE,oBAAoB;IACxE,4BAA4B,EAAE,4BAA4B,EAAE,wBAAwB;IACpF,sBAAsB,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,oBAAoB;IACxF,6BAA6B,EAAE,0BAA0B,EAAE,2BAA2B;IACtF,gCAAgC,EAAE,wCAAwC,EAAE,uCAAuC;IACnH,2BAA2B,EAAE,+BAA+B,EAAE,2BAA2B;IACzF,yBAAyB,EAAE,sBAAsB,EAAE,qBAAqB;IACxE,wBAAwB,EAAE,2BAA2B,EAAE,uBAAuB;IAC9E,0BAA0B,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,sCAAsC,EAAE,2CAA2C;IACtK,6BAA6B,EAAE,mCAAmC,EAAE,6BAA6B;IACjG,yBAAyB,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,oCAAoC,EAAE,0BAA0B;IACrJ,qBAAqB,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,0BAA0B;IAC/F,kBAAkB,EAAE,0BAA0B,EAAE,8BAA8B;IAC9E,sCAAsC,EAAE,mCAAmC;IAC3E,kCAAkC,EAAE,iCAAiC;IACrE,0BAA0B,EAAE,mCAAmC,EAAE,qBAAqB;IACtF,4BAA4B,EAAE,+BAA+B,EAAE,+BAA+B;IAC9F,+BAA+B,EAAE,gCAAgC,EAAE,6BAA6B;IAChG,sCAAsC,EAAE,kCAAkC,EAAE,0BAA0B;IACtG,4CAA4C;IAC5C,qBAAqB,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,2BAA2B,EAAE,sBAAsB;IACrI,6BAA6B,EAAE,gCAAgC,EAAE,6BAA6B;CACtF,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ProfileSpan {
|
|
2
|
+
t: number;
|
|
3
|
+
profile: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const PROFILE_TIMELINE_FILE = "codex-profile-timeline.json";
|
|
6
|
+
export declare const PROFILE_TIMELINE_MAX = 200;
|
|
7
|
+
export declare const PROFILE_UNRECORDED = "unrecorded";
|
|
8
|
+
/** The file's entries, checked and sorted (an unreadable or missing file: none). */
|
|
9
|
+
export declare function readProfileTimeline(path: string): Promise<ProfileSpan[]>;
|
|
10
|
+
/**
|
|
11
|
+
* A launch under `profile` at `t`. One write at a time per file; nothing is added when the last entry already names the
|
|
12
|
+
* same profile. Never throws: a lost entry leaves that run's turns under the previous entry's profile (or unrecorded).
|
|
13
|
+
*/
|
|
14
|
+
export declare function recordProfileLaunch(path: string, profile: string, t?: number): Promise<void>;
|
|
15
|
+
/** The profile in effect at `t`: the latest entry at or before it, else "unrecorded". */
|
|
16
|
+
export declare function profileAt(spans: readonly ProfileSpan[], t: number): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3066 §1d: which credential profile a Codex instance ran under, and since when — so its usage is told by account. A
|
|
3
|
+
* Codex rollout does not say which login wrote it (so its hours were all "unrecorded"); AgEnD does know which profile it
|
|
4
|
+
* launched each Codex under. Each launch appends `{ t, profile }` to the instance's `codex-profile-timeline.json`; the
|
|
5
|
+
* usage scan tags each turn with the profile in effect at its time (the latest entry at or before it). A switch of
|
|
6
|
+
* profile is a relaunch, so a turn is never between two entries of one run. Turns from before the first entry stay
|
|
7
|
+
* "unrecorded" — history is never guessed.
|
|
8
|
+
*
|
|
9
|
+
* Small (≤ 200 entries, the oldest dropped) and written off the fleet's thread, atomically.
|
|
10
|
+
*/
|
|
11
|
+
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
12
|
+
export const PROFILE_TIMELINE_FILE = "codex-profile-timeline.json";
|
|
13
|
+
export const PROFILE_TIMELINE_MAX = 200;
|
|
14
|
+
export const PROFILE_UNRECORDED = "unrecorded";
|
|
15
|
+
/** The file's entries, checked and sorted (an unreadable or missing file: none). */
|
|
16
|
+
export async function readProfileTimeline(path) {
|
|
17
|
+
let v;
|
|
18
|
+
try {
|
|
19
|
+
v = JSON.parse(await readFile(path, "utf8"));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const list = v && typeof v === "object" && !Array.isArray(v) && Array.isArray(v.spans) ? v.spans : [];
|
|
25
|
+
return list.filter((e) => !!e && typeof e === "object" && typeof e.t === "number"
|
|
26
|
+
&& Number.isFinite(e.t) && typeof e.profile === "string" && e.profile !== "")
|
|
27
|
+
.map((e) => ({ t: e.t, profile: e.profile })).sort((a, b) => a.t - b.t);
|
|
28
|
+
}
|
|
29
|
+
const writes = new Map();
|
|
30
|
+
/**
|
|
31
|
+
* A launch under `profile` at `t`. One write at a time per file; nothing is added when the last entry already names the
|
|
32
|
+
* same profile. Never throws: a lost entry leaves that run's turns under the previous entry's profile (or unrecorded).
|
|
33
|
+
*/
|
|
34
|
+
export function recordProfileLaunch(path, profile, t = Date.now()) {
|
|
35
|
+
const next = (writes.get(path) ?? Promise.resolve()).then(async () => {
|
|
36
|
+
const spans = await readProfileTimeline(path);
|
|
37
|
+
if (spans.at(-1)?.profile === profile)
|
|
38
|
+
return;
|
|
39
|
+
const kept = [...spans, { t, profile }].slice(-PROFILE_TIMELINE_MAX);
|
|
40
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
41
|
+
await writeFile(tmp, JSON.stringify({ v: 1, spans: kept }), { mode: 0o600 });
|
|
42
|
+
await rename(tmp, path);
|
|
43
|
+
}).catch(() => { });
|
|
44
|
+
writes.set(path, next);
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
/** The profile in effect at `t`: the latest entry at or before it, else "unrecorded". */
|
|
48
|
+
export function profileAt(spans, t) {
|
|
49
|
+
let found = PROFILE_UNRECORDED;
|
|
50
|
+
for (const s of spans) {
|
|
51
|
+
if (s.t <= t)
|
|
52
|
+
found = s.profile;
|
|
53
|
+
else
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=profile-timeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile-timeline.js","sourceRoot":"","sources":["../../src/usage/profile-timeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ProviderUsage } from "./providers.js";
|
|
2
|
+
export interface QuotaReading {
|
|
3
|
+
/** When the reading was made (ms). */
|
|
4
|
+
t: number;
|
|
5
|
+
/** The usage row's id: "claude", "claude:hsinlin", "codex:yanxianchen", … — one account. */
|
|
6
|
+
provider: string;
|
|
7
|
+
/** The credential profile, when the row has one. */
|
|
8
|
+
profile?: string;
|
|
9
|
+
/** The metric's label: "Weekly", "Sonnet (weekly)", … */
|
|
10
|
+
metric: string;
|
|
11
|
+
/** "model": the vendor's per-model quota (UsageMetric.scope) — its label names the model. Absent: not one. */
|
|
12
|
+
scope?: "model";
|
|
13
|
+
/** Percent used (0–100, as the vendor reports it). */
|
|
14
|
+
used: number;
|
|
15
|
+
/** When this window resets (ISO), or null when the vendor did not say. */
|
|
16
|
+
resetsAt: string | null;
|
|
17
|
+
windowMs: number | null;
|
|
18
|
+
}
|
|
19
|
+
export declare const QUOTA_HISTORY_KEEP_MS: number;
|
|
20
|
+
export declare const QUOTA_HISTORY_MAX_BYTES: number;
|
|
21
|
+
/**
|
|
22
|
+
* The same reset (Fable r1 #5): a vendor that gives only "resets in N seconds" yields a time that drifts by the read's own
|
|
23
|
+
* timing; within 10 minutes it is the same window, never a new line on every read. A real reset moves it by days.
|
|
24
|
+
*/
|
|
25
|
+
export declare const RESET_TOLERANCE_MS: number;
|
|
26
|
+
export declare function sameReset(a: string | null, b: string | null): boolean;
|
|
27
|
+
/** A line from the file, checked whole (it is not trusted): null when it is not a reading. */
|
|
28
|
+
export declare function parseQuotaReading(line: string): QuotaReading | null;
|
|
29
|
+
/** The weekly-or-longer percent metrics of a good read, as readings at `now`. */
|
|
30
|
+
export declare function quotaReadingsOf(p: ProviderUsage, now: number): QuotaReading[];
|
|
31
|
+
/**
|
|
32
|
+
* Record a good read's weekly readings: a line for each metric whose percent or reset time changed since its last line.
|
|
33
|
+
* Never throws and never blocks the caller: the write is queued (one at a time) and a failure only loses this reading.
|
|
34
|
+
*/
|
|
35
|
+
export declare function recordQuotaReadings(p: ProviderUsage, now?: number): void;
|
|
36
|
+
/** Every reading kept, oldest first (§1 reads it). Unreadable lines are skipped. */
|
|
37
|
+
export declare function readQuotaHistory(): Promise<QuotaReading[]>;
|
|
38
|
+
/** Tests: write to `path` (null: nowhere), starting from what that file holds; resolves when queued writes are done. */
|
|
39
|
+
export declare function setQuotaHistoryFileForTests(path: string | null): Promise<void>;
|
|
40
|
+
/** Tests (usage-api's fetcher seam): record nothing, at once — never into the real AGEND_HOME. */
|
|
41
|
+
export declare function quotaHistoryOffForTests(): void;
|
|
42
|
+
/** Tests: wait for the queued writes. */
|
|
43
|
+
export declare function flushQuotaHistory(): Promise<void>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3066 §1d: the weekly quota readings over time — the history the capacity estimate (§1) matches against the tokens each
|
|
3
|
+
* account used. Only the readings AgEnD already makes (get_usage, the usage page, the Discord presence refresh, the
|
|
4
|
+
* 30-minute background read): never an extra vendor call of its own.
|
|
5
|
+
*
|
|
6
|
+
* One JSON line per change: a weekly metric's percent or its reset time moved since the last line for that (provider,
|
|
7
|
+
* metric). A week is at most ~100 steps of a whole percent, so ≤ ~110 lines an account-week (~1–3 KB). Kept 12 weeks
|
|
8
|
+
* (older lines are dropped on the first write of each day) and never over 2 MB (the oldest lines go first, whenever the
|
|
9
|
+
* file is over it). A reset time within 10 minutes of the last is the same reset. Written off the fleet's thread
|
|
10
|
+
* (fs/promises), one write at a time.
|
|
11
|
+
* `usage-quota-history.jsonl` in AGEND_HOME.
|
|
12
|
+
*/
|
|
13
|
+
import { appendFile, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { getAgendHome } from "../paths.js";
|
|
16
|
+
export const QUOTA_HISTORY_KEEP_MS = 12 * 7 * 24 * 3_600_000;
|
|
17
|
+
export const QUOTA_HISTORY_MAX_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
/** A window of a day or longer is a quota worth matching (the five-hour session window is not). */
|
|
19
|
+
const MIN_WINDOW_MS = 24 * 3_600_000;
|
|
20
|
+
let fileOf = () => join(getAgendHome(), "usage-quota-history.jsonl");
|
|
21
|
+
/** The last reading written per (provider, metric): what a new one is compared with. */
|
|
22
|
+
const last = new Map();
|
|
23
|
+
let loaded = false;
|
|
24
|
+
let chain = Promise.resolve();
|
|
25
|
+
const keyOf = (provider, metric) => `${provider}\u0000${metric}`;
|
|
26
|
+
/**
|
|
27
|
+
* The same reset (Fable r1 #5): a vendor that gives only "resets in N seconds" yields a time that drifts by the read's own
|
|
28
|
+
* timing; within 10 minutes it is the same window, never a new line on every read. A real reset moves it by days.
|
|
29
|
+
*/
|
|
30
|
+
export const RESET_TOLERANCE_MS = 10 * 60_000;
|
|
31
|
+
export function sameReset(a, b) {
|
|
32
|
+
if (a === b)
|
|
33
|
+
return true;
|
|
34
|
+
if (a === null || b === null)
|
|
35
|
+
return false;
|
|
36
|
+
const ta = Date.parse(a), tb = Date.parse(b);
|
|
37
|
+
return Number.isFinite(ta) && Number.isFinite(tb) && Math.abs(ta - tb) < RESET_TOLERANCE_MS;
|
|
38
|
+
}
|
|
39
|
+
/** A line from the file, checked whole (it is not trusted): null when it is not a reading. */
|
|
40
|
+
export function parseQuotaReading(line) {
|
|
41
|
+
let v;
|
|
42
|
+
try {
|
|
43
|
+
v = JSON.parse(line);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
49
|
+
return null;
|
|
50
|
+
const r = v;
|
|
51
|
+
if (typeof r.t !== "number" || !Number.isFinite(r.t) || typeof r.provider !== "string" || typeof r.metric !== "string"
|
|
52
|
+
|| typeof r.used !== "number" || !Number.isFinite(r.used) || !(r.resetsAt === null || typeof r.resetsAt === "string")
|
|
53
|
+
|| !(r.windowMs === null || (typeof r.windowMs === "number" && Number.isFinite(r.windowMs)))
|
|
54
|
+
|| !(r.profile === undefined || typeof r.profile === "string") || !(r.scope === undefined || r.scope === "model"))
|
|
55
|
+
return null;
|
|
56
|
+
return { t: r.t, provider: r.provider, ...(r.profile !== undefined ? { profile: r.profile } : {}), metric: r.metric, ...(r.scope === "model" ? { scope: "model" } : {}), used: r.used,
|
|
57
|
+
resetsAt: r.resetsAt, windowMs: r.windowMs };
|
|
58
|
+
}
|
|
59
|
+
/** The weekly-or-longer percent metrics of a good read, as readings at `now`. */
|
|
60
|
+
export function quotaReadingsOf(p, now) {
|
|
61
|
+
if (p.status !== "ok")
|
|
62
|
+
return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const m of p.metrics) {
|
|
65
|
+
if (m.type !== "percent" || typeof m.used !== "number" || !Number.isFinite(m.used))
|
|
66
|
+
continue;
|
|
67
|
+
if (typeof m.windowMs !== "number" || m.windowMs < MIN_WINDOW_MS)
|
|
68
|
+
continue;
|
|
69
|
+
out.push({ t: now, provider: p.id, ...(p.profile ? { profile: p.profile } : {}), metric: m.label, ...(m.scope === "model" ? { scope: "model" } : {}), used: m.used,
|
|
70
|
+
resetsAt: typeof m.resetsAt === "string" ? m.resetsAt : null, windowMs: m.windowMs });
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
async function load(path) {
|
|
75
|
+
if (loaded)
|
|
76
|
+
return;
|
|
77
|
+
loaded = true;
|
|
78
|
+
let text = "";
|
|
79
|
+
try {
|
|
80
|
+
text = await readFile(path, "utf8");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return;
|
|
84
|
+
} // none yet
|
|
85
|
+
for (const line of text.split("\n")) {
|
|
86
|
+
const r = line ? parseQuotaReading(line) : null;
|
|
87
|
+
if (r)
|
|
88
|
+
last.set(keyOf(r.provider, r.metric), { used: r.used, resetsAt: r.resetsAt });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** When lines older than 12 weeks were last dropped: once a day (Fable r1 #3), and whenever the file is over its cap. */
|
|
92
|
+
let lastAgePrune = Number.NEGATIVE_INFINITY;
|
|
93
|
+
const AGE_PRUNE_EVERY_MS = 24 * 3_600_000;
|
|
94
|
+
/** Keep the last 12 weeks, and the file under its cap (oldest first out). Rewritten atomically. */
|
|
95
|
+
async function prune(path, now) {
|
|
96
|
+
let size = 0;
|
|
97
|
+
try {
|
|
98
|
+
size = (await stat(path)).size;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (size <= QUOTA_HISTORY_MAX_BYTES && now - lastAgePrune < AGE_PRUNE_EVERY_MS)
|
|
104
|
+
return;
|
|
105
|
+
lastAgePrune = now;
|
|
106
|
+
const lines = (await readFile(path, "utf8")).split("\n").filter((l) => {
|
|
107
|
+
const r = l ? parseQuotaReading(l) : null;
|
|
108
|
+
return r !== null && r.t >= now - QUOTA_HISTORY_KEEP_MS;
|
|
109
|
+
});
|
|
110
|
+
let bytes = lines.reduce((n, l) => n + Buffer.byteLength(l) + 1, 0), from = 0;
|
|
111
|
+
while (bytes > QUOTA_HISTORY_MAX_BYTES * 0.9 && from < lines.length)
|
|
112
|
+
bytes -= Buffer.byteLength(lines[from++]) + 1;
|
|
113
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
114
|
+
await writeFile(tmp, lines.slice(from).map((l) => `${l}\n`).join(""), { mode: 0o600 });
|
|
115
|
+
await rename(tmp, path);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Record a good read's weekly readings: a line for each metric whose percent or reset time changed since its last line.
|
|
119
|
+
* Never throws and never blocks the caller: the write is queued (one at a time) and a failure only loses this reading.
|
|
120
|
+
*/
|
|
121
|
+
export function recordQuotaReadings(p, now = Date.now()) {
|
|
122
|
+
if (!fileOf)
|
|
123
|
+
return;
|
|
124
|
+
const readings = quotaReadingsOf(p, now);
|
|
125
|
+
if (!readings.length)
|
|
126
|
+
return;
|
|
127
|
+
const path = fileOf();
|
|
128
|
+
chain = chain.then(async () => {
|
|
129
|
+
await load(path);
|
|
130
|
+
const fresh = readings.filter((r) => {
|
|
131
|
+
const was = last.get(keyOf(r.provider, r.metric));
|
|
132
|
+
return !was || was.used !== r.used || !sameReset(was.resetsAt, r.resetsAt);
|
|
133
|
+
});
|
|
134
|
+
if (!fresh.length)
|
|
135
|
+
return;
|
|
136
|
+
await appendFile(path, fresh.map((r) => `${JSON.stringify(r)}\n`).join(""), { mode: 0o600 });
|
|
137
|
+
for (const r of fresh)
|
|
138
|
+
last.set(keyOf(r.provider, r.metric), { used: r.used, resetsAt: r.resetsAt });
|
|
139
|
+
await prune(path, now);
|
|
140
|
+
}).catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
/** Every reading kept, oldest first (§1 reads it). Unreadable lines are skipped. */
|
|
143
|
+
export async function readQuotaHistory() {
|
|
144
|
+
if (!fileOf)
|
|
145
|
+
return [];
|
|
146
|
+
await chain;
|
|
147
|
+
let text = "";
|
|
148
|
+
try {
|
|
149
|
+
text = await readFile(fileOf(), "utf8");
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
return text.split("\n").map((l) => (l ? parseQuotaReading(l) : null)).filter((r) => r !== null).sort((a, b) => a.t - b.t);
|
|
155
|
+
}
|
|
156
|
+
/** Tests: write to `path` (null: nowhere), starting from what that file holds; resolves when queued writes are done. */
|
|
157
|
+
export async function setQuotaHistoryFileForTests(path) {
|
|
158
|
+
await chain;
|
|
159
|
+
fileOf = path === null ? null : () => path;
|
|
160
|
+
last.clear();
|
|
161
|
+
loaded = false;
|
|
162
|
+
lastAgePrune = Number.NEGATIVE_INFINITY;
|
|
163
|
+
}
|
|
164
|
+
/** Tests (usage-api's fetcher seam): record nothing, at once — never into the real AGEND_HOME. */
|
|
165
|
+
export function quotaHistoryOffForTests() { fileOf = null; last.clear(); loaded = false; }
|
|
166
|
+
/** Tests: wait for the queued writes. */
|
|
167
|
+
export function flushQuotaHistory() { return chain; }
|
|
168
|
+
//# sourceMappingURL=quota-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota-history.js","sourceRoot":"","sources":["../../src/usage/quota-history.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,6 +15,8 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
15
15
|
import type { FleetConfig } from "../types.js";
|
|
16
16
|
import type { Logger } from "pino";
|
|
17
17
|
import { type ProviderUsage, type UsageMetric } from "./providers.js";
|
|
18
|
+
import type { CapacityBasis, CapacityEstimate } from "./capacity.js";
|
|
19
|
+
import type { UsageI18nRef } from "./providers.js";
|
|
18
20
|
export interface UsageApiContext {
|
|
19
21
|
readonly fleetConfig: FleetConfig | null;
|
|
20
22
|
readonly logger: Logger;
|
|
@@ -50,6 +52,35 @@ export declare function setUsageLastGoodFileForTests(path: string | null): Promi
|
|
|
50
52
|
export declare function getUsageSnapshot(force?: boolean, providerIds?: Iterable<string>, opts?: {
|
|
51
53
|
allowStale?: boolean;
|
|
52
54
|
}): Promise<UsagePayload>;
|
|
55
|
+
/** A weekly metric's measured 100%, as served: the numbers, and its lines in words (text + key, like a metric's note). */
|
|
56
|
+
export interface UsageCapacity {
|
|
57
|
+
status: "ok" | "not_enough";
|
|
58
|
+
deltaPct: number;
|
|
59
|
+
windows?: number;
|
|
60
|
+
basis?: CapacityBasis;
|
|
61
|
+
median?: number;
|
|
62
|
+
low?: number;
|
|
63
|
+
high?: number;
|
|
64
|
+
/** All tokens at 100% (median), beside an API-equivalent line. */
|
|
65
|
+
tokens?: number;
|
|
66
|
+
bestFit?: CapacityBasis | null;
|
|
67
|
+
mixed?: boolean;
|
|
68
|
+
line: {
|
|
69
|
+
text: string;
|
|
70
|
+
i18n: UsageI18nRef;
|
|
71
|
+
};
|
|
72
|
+
notes: Array<{
|
|
73
|
+
text: string;
|
|
74
|
+
i18n: UsageI18nRef;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
export declare function setCapacityLookup(fn: ((provider: string, metric: string) => CapacityEstimate | null) | null): void;
|
|
78
|
+
export declare const compactTokens: (n: number) => string;
|
|
79
|
+
/**
|
|
80
|
+
* The words for an estimate (English text; the page shows its own language from the key). Null when an "ok" estimate
|
|
81
|
+
* has no basis (Prism #3073 P1: estimateCapacity never makes one, but a read must not fail on it).
|
|
82
|
+
*/
|
|
83
|
+
export declare function capacityText(est: CapacityEstimate): UsageCapacity | null;
|
|
53
84
|
/**
|
|
54
85
|
* Render a usage payload as one compact chat message.
|
|
55
86
|
*
|
package/dist/usage/usage-api.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getAgendHome } from "../paths.js";
|
|
|
7
7
|
import { t } from "../locale.js";
|
|
8
8
|
import { usageExpiryText, usageResetSinceText, usageResetText, usageRowName, usageStaleText, usageText } from "./i18n.js";
|
|
9
9
|
import { guestRequestOf } from "../web-guest-access.js";
|
|
10
|
+
import { quotaHistoryOffForTests, recordQuotaReadings } from "./quota-history.js";
|
|
10
11
|
/** Keep primary account limits even at zero; hide only unused per-model noise. */
|
|
11
12
|
export function isVisibleUsageMetric(metric) {
|
|
12
13
|
return !(metric.scope === "model"
|
|
@@ -114,6 +115,7 @@ export function setUsageFetcherForTests(fn) {
|
|
|
114
115
|
lastGood.clear();
|
|
115
116
|
lastGoodFile = null;
|
|
116
117
|
lastGoodLoaded = true;
|
|
118
|
+
quotaHistoryOffForTests();
|
|
117
119
|
retryAfterUntil.clear();
|
|
118
120
|
}
|
|
119
121
|
/** Tests: persist lastGood at `path` (read it on the next use), or not at all (null). */
|
|
@@ -267,6 +269,7 @@ function withStaleFallback(payload) {
|
|
|
267
269
|
const providers = payload.providers.map(p => {
|
|
268
270
|
if (p.status === "ok" && p.metrics.length > 0) {
|
|
269
271
|
lastGood.set(p.id, { at: now, provider: p });
|
|
272
|
+
recordQuotaReadings(p, now); // #3066 §1d: the weekly percent over time (only when it moved)
|
|
270
273
|
changed = true;
|
|
271
274
|
return p;
|
|
272
275
|
}
|
|
@@ -410,7 +413,83 @@ function fetchFor(owner, effectiveForce, again) {
|
|
|
410
413
|
* second entry point that bypassed it would defeat that.
|
|
411
414
|
*/
|
|
412
415
|
export async function getUsageSnapshot(force = false, providerIds, opts = {}) {
|
|
413
|
-
return filterUsageProviders(reageStale(await usage(force, opts.allowStale === true), Date.now()), providerIds, { hostLogins: true });
|
|
416
|
+
return withCapacity(filterUsageProviders(reageStale(await usage(force, opts.allowStale === true), Date.now()), providerIds, { hostLogins: true }));
|
|
417
|
+
}
|
|
418
|
+
/** Published by the fleet (FleetManager.refreshCapacity): the estimate for a provider's metric, read synchronously. */
|
|
419
|
+
let capacityLookup = null;
|
|
420
|
+
export function setCapacityLookup(fn) { capacityLookup = fn; }
|
|
421
|
+
const money0 = (n) => `$${n >= 100 ? Math.round(n).toLocaleString("en-US") : n.toFixed(2)}`;
|
|
422
|
+
export const compactTokens = (n) => (n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(Math.round(n)));
|
|
423
|
+
const ref = (key, args) => ({ key: key, args });
|
|
424
|
+
const fill = (text, args) => args.reduce((s, a, i) => s.split(`{${i}}`).join(String(a)), text);
|
|
425
|
+
/**
|
|
426
|
+
* The words for an estimate (English text; the page shows its own language from the key). Null when an "ok" estimate
|
|
427
|
+
* has no basis (Prism #3073 P1: estimateCapacity never makes one, but a read must not fail on it).
|
|
428
|
+
*/
|
|
429
|
+
export function capacityText(est) {
|
|
430
|
+
const onlyAgend = { text: "Counts only this fleet's agents; use of the same account elsewhere makes this low.", i18n: ref("usage.capacity.only_agend", []) };
|
|
431
|
+
if (est.status === "not_enough") {
|
|
432
|
+
const args = [est.deltaPct];
|
|
433
|
+
return { status: "not_enough", deltaPct: est.deltaPct, line: { text: fill("Not enough usage yet to measure 100% (Δ {0}% of 10%)", args), i18n: ref("usage.capacity.not_enough", args) }, notes: [onlyAgend] };
|
|
434
|
+
}
|
|
435
|
+
const api = est.bases.api, tokensBasis = est.bases.all ?? est.bases.output;
|
|
436
|
+
if (!api && !tokensBasis)
|
|
437
|
+
return null;
|
|
438
|
+
const basis = api ? "api" : est.bases.all ? "all" : "output";
|
|
439
|
+
const chosen = est.bases[basis];
|
|
440
|
+
let line;
|
|
441
|
+
if (api && est.bases.all) {
|
|
442
|
+
const args = [money0(api.median), money0(api.low), money0(api.high), compactTokens(est.bases.all.median), api.windows];
|
|
443
|
+
line = { text: fill("100% ≈ {0} ({1}–{2}) · ≈ {3} tokens · from {4} weeks", args), i18n: ref("usage.capacity.line", args) };
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
const args = [compactTokens(chosen.median), compactTokens(chosen.low), compactTokens(chosen.high), chosen.windows];
|
|
447
|
+
line = { text: fill("100% ≈ {0} tokens ({1}–{2}) · from {3} weeks", args), i18n: ref("usage.capacity.line_tokens", args) };
|
|
448
|
+
}
|
|
449
|
+
const notes = [onlyAgend];
|
|
450
|
+
const fit = est.bestFit ? est.bases[est.bestFit] : undefined;
|
|
451
|
+
if (est.bestFit && fit && fit.cv !== null) {
|
|
452
|
+
const args = [Math.round(fit.cv * 100), fit.windows];
|
|
453
|
+
const sentence = { api: "API-equivalent fits best: ±{0}% across {1} weeks", output: "Output tokens fit best: ±{0}% across {1} weeks", all: "All tokens fit best: ±{0}% across {1} weeks" }[est.bestFit];
|
|
454
|
+
notes.push({ text: fill(sentence, args), i18n: ref(`usage.capacity.best_fit_${est.bestFit}`, args) });
|
|
455
|
+
}
|
|
456
|
+
if (est.mixed)
|
|
457
|
+
notes.push({ text: "Bases disagree — possibly used outside this fleet", i18n: ref("usage.capacity.mixed", []) });
|
|
458
|
+
return { status: "ok", deltaPct: est.deltaPct, windows: chosen.windows, basis, median: chosen.median, low: chosen.low, high: chosen.high,
|
|
459
|
+
...(api && est.bases.all ? { tokens: est.bases.all.median } : {}), bestFit: est.bestFit, mixed: est.mixed, line, notes };
|
|
460
|
+
}
|
|
461
|
+
/** Each weekly percent metric with an estimate gets `capacity` — on copies: the cached payload stays as read. */
|
|
462
|
+
function withCapacity(payload) {
|
|
463
|
+
const look = capacityLookup;
|
|
464
|
+
if (!look)
|
|
465
|
+
return payload;
|
|
466
|
+
let any = false;
|
|
467
|
+
const providers = payload.providers.map((p) => {
|
|
468
|
+
if (p.status !== "ok")
|
|
469
|
+
return p;
|
|
470
|
+
let changed = false;
|
|
471
|
+
const metrics = p.metrics.map((m) => {
|
|
472
|
+
if (m.type !== "percent" || typeof m.windowMs !== "number" || m.windowMs < 24 * 3_600_000)
|
|
473
|
+
return m;
|
|
474
|
+
let capacity = null;
|
|
475
|
+
try {
|
|
476
|
+
const est = look(p.id, m.label);
|
|
477
|
+
capacity = est ? capacityText(est) : null;
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
capacity = null;
|
|
481
|
+
}
|
|
482
|
+
if (!capacity)
|
|
483
|
+
return m; // an estimate that cannot be put in words never fails the read
|
|
484
|
+
changed = true;
|
|
485
|
+
return { ...m, capacity };
|
|
486
|
+
});
|
|
487
|
+
if (!changed)
|
|
488
|
+
return p;
|
|
489
|
+
any = true;
|
|
490
|
+
return { ...p, metrics };
|
|
491
|
+
});
|
|
492
|
+
return any ? { ...payload, providers } : payload;
|
|
414
493
|
}
|
|
415
494
|
/**
|
|
416
495
|
* Last known numbers age while they are served (Prism #2257 P2): a snapshot is reused for minutes, and a window may
|