@omnicross/daemon 0.2.1 → 0.3.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 -21
- package/README.md +19 -19
- package/dist/cli.cjs +1761 -337
- package/dist/cli.js +1698 -260
- package/dist/index.cjs +1702 -320
- package/dist/index.d.cts +334 -28
- package/dist/index.d.ts +334 -28
- package/dist/index.js +1637 -246
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { AllowanceSchedulingConfig, AccountProbeConfig, OutboundKeyDb as Outboun
|
|
|
5
5
|
import { RouteLeaseManager, ProviderProxy } from '@omnicross/core/provider-proxy';
|
|
6
6
|
import { UsageRecorder, PricingEngine } from '@omnicross/core/usage';
|
|
7
7
|
import { SubscriptionCredentialStore, FetchLike, CodexImageCapabilityEvidenceSource, CodexImageCapabilityEvidenceRequest, CodexImageCapabilityEvidence, CodexImageCapabilityObservation, ImageExecutionScheduler, ImageExecutionAccountKey, ImageExecutionSchedulerRequest, ImageExecutionSchedulerGrant, SubscriptionAccountService, SubscriptionProviderRegistry } from '@omnicross/subscriptions';
|
|
8
|
-
import { AccountAllowanceSnapshot } from '@omnicross/contracts/account-allowance-types';
|
|
9
|
-
import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, AccountTokensConfig, ProxyConfig, SubscriptionAccountSanitized, AccountClientIdentity, SubscriptionAccountEntry } from '@omnicross/contracts/account-tokens-types';
|
|
8
|
+
import { AccountAllowanceSnapshot, AllowanceWindow } from '@omnicross/contracts/account-allowance-types';
|
|
9
|
+
import { ClaudeTokenConfig, CodexTokenConfig, GeminiTokenConfig, KimiTokenConfig, AccountTokensConfig, ProxyConfig, SubscriptionAccountSanitized, AccountClientIdentity, SubscriptionAccountEntry } from '@omnicross/contracts/account-tokens-types';
|
|
10
10
|
import * as _omnicross_contracts_subscription_types from '@omnicross/contracts/subscription-types';
|
|
11
11
|
import { OpenCodeGoTokenConfig, SubscriptionProviderId } from '@omnicross/contracts/subscription-types';
|
|
12
12
|
import { AccountAllowanceStore } from '@omnicross/core/pipeline/AccountAllowanceStore';
|
|
@@ -626,6 +626,32 @@ declare function loadConfig(path: string): DaemonConfig;
|
|
|
626
626
|
* `enc:` untouched) before serializing. */
|
|
627
627
|
declare function saveConfig(path: string, cfg: DaemonConfig): void;
|
|
628
628
|
|
|
629
|
+
/**
|
|
630
|
+
* atomicFile — the daemon's shared ATOMIC file-replace for credential-bearing
|
|
631
|
+
* state files (`keys.json`, `tokens.json`, …).
|
|
632
|
+
*
|
|
633
|
+
* A bare `writeFileSync(target, …)` is truncate-then-write: a crash, ENOSPC,
|
|
634
|
+
* or power loss mid-write leaves the target HALF-WRITTEN and the prior content
|
|
635
|
+
* is gone. For `tokens.json` that is account data — the 2026-09-06 incident
|
|
636
|
+
* lost every stored OAuth account exactly this way (a truncated file that the
|
|
637
|
+
* reader then silently treated as "no accounts"). Same-directory temp-write +
|
|
638
|
+
* fsync + rename means a FAILED write only ever discards the temp file; the
|
|
639
|
+
* prior target survives byte-equal.
|
|
640
|
+
*
|
|
641
|
+
* The temp name embeds pid + random bytes (never collides with a live temp of
|
|
642
|
+
* another process) and lives in the target's own directory so the final
|
|
643
|
+
* `renameSync` never crosses a volume (POSIX and win32 both make the replace
|
|
644
|
+
* atomic there; Node maps rename-over-existing to MoveFileEx with
|
|
645
|
+
* REPLACE_EXISTING on Windows).
|
|
646
|
+
*
|
|
647
|
+
* Extracted verbatim from `JsonOutboundKeyDb` (its original + only user) so
|
|
648
|
+
* every file-backed port shares ONE implementation instead of re-deriving it.
|
|
649
|
+
*
|
|
650
|
+
* @module @omnicross/daemon/ports/atomicFile
|
|
651
|
+
*/
|
|
652
|
+
/** Injectable seam (tests pass a throwing/stubbed replace to prove the prior file survives). */
|
|
653
|
+
type AtomicFileReplace = (targetPath: string, contents: string) => void;
|
|
654
|
+
|
|
629
655
|
/**
|
|
630
656
|
* account-multi — daemon-side pure helpers for the subscription multi-account
|
|
631
657
|
* layout.
|
|
@@ -640,7 +666,7 @@ declare function saveConfig(path: string, cfg: DaemonConfig): void;
|
|
|
640
666
|
*/
|
|
641
667
|
|
|
642
668
|
/** Provider id → owned contract field names. */
|
|
643
|
-
type DaemonProvider = 'claude' | 'codex' | 'gemini' | 'opencodego';
|
|
669
|
+
type DaemonProvider = 'claude' | 'codex' | 'gemini' | 'opencodego' | 'kimi';
|
|
644
670
|
interface AccountMetadataPatch {
|
|
645
671
|
label?: string;
|
|
646
672
|
enabled?: boolean;
|
|
@@ -710,9 +736,14 @@ type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentia
|
|
|
710
736
|
* over a sibling `tokens.json` holding an `AccountTokensConfig`-shaped object
|
|
711
737
|
* (`{ claude?, codex?, gemini?, opencodego?, updatedAt }`). Modeled on
|
|
712
738
|
* `JsonOutboundKeyDb`: the constructor takes the path; reads are
|
|
713
|
-
* `existsSync` `readFileSync` `JSON.parse
|
|
714
|
-
*
|
|
715
|
-
*
|
|
739
|
+
* `existsSync` `readFileSync` `JSON.parse`. A MISSING file returns a minimal
|
|
740
|
+
* `{ updatedAt }` config (first boot; the strategies already guard
|
|
741
|
+
* `?.accessToken`, so a partial/empty config never crashes dispatch). A file
|
|
742
|
+
* that EXISTS but will not parse as a JSON object is first QUARANTINED to a
|
|
743
|
+
* sibling `tokens.json.corrupt-<stamp>` backup (see `quarantineCorrupt`) and
|
|
744
|
+
* only then treated as empty — a corrupt file is DATA, not "no accounts"
|
|
745
|
+
* (2026-09-06 incident: a truncated write read as empty, then the re-login
|
|
746
|
+
* persist overwrote the only copy of every stored account).
|
|
716
747
|
*
|
|
717
748
|
* The PORT surface is read-only by design: the codex / gemini strategies pull
|
|
718
749
|
* their access token via `getFullConfig().<provider>.accessToken`; only claude /
|
|
@@ -736,6 +767,12 @@ type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentia
|
|
|
736
767
|
* at-rest with NO extra work (the store API guarantees it). The "re-read on every
|
|
737
768
|
* call, no cache" semantics are unchanged.
|
|
738
769
|
*
|
|
770
|
+
* DURABILITY: `persist` writes through the shared same-directory temp + fsync +
|
|
771
|
+
* rename (`atomicReplaceUtf8`) — a crashed/failed/interrupted write discards
|
|
772
|
+
* only the temp file and the prior `tokens.json` survives byte-equal. This is
|
|
773
|
+
* the other half of the 2026-09-06 fix: the store previously used a bare
|
|
774
|
+
* truncate-then-write `writeFileSync`, which could leave a half-written file.
|
|
775
|
+
*
|
|
739
776
|
* REAL TOKEN REFRESH (oauth design D4): `refresh{Claude,Codex,Gemini}Token` mint
|
|
740
777
|
* a new access token via the shared host-clean OAuth refresh functions
|
|
741
778
|
* (`@omnicross/subscriptions/oauth`, injected `FetchLike` default global
|
|
@@ -758,13 +795,15 @@ type ExternalCliReader = (provider: ExternalCliProvider) => ExternalCliCredentia
|
|
|
758
795
|
* shapes), keyed by `SubscriptionProviderId` the daemon admin layer validates
|
|
759
796
|
* the wire body to one of these before calling the writer.
|
|
760
797
|
*/
|
|
761
|
-
type SubscriptionTokenBlock = ClaudeTokenConfig | CodexTokenConfig | GeminiTokenConfig | OpenCodeGoTokenConfig;
|
|
798
|
+
type SubscriptionTokenBlock = ClaudeTokenConfig | CodexTokenConfig | GeminiTokenConfig | OpenCodeGoTokenConfig | KimiTokenConfig;
|
|
762
799
|
declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialStore {
|
|
763
800
|
private readonly tokensPath;
|
|
764
801
|
private readonly box;
|
|
765
802
|
private readonly fetchImpl;
|
|
766
803
|
/** Injectable, strictly read-only external CLI native-store reader. */
|
|
767
804
|
private readonly externalCliReader;
|
|
805
|
+
/** Injectable atomic-replace seam (tests prove a failed write keeps the prior file). */
|
|
806
|
+
private readonly atomicReplace;
|
|
768
807
|
/**
|
|
769
808
|
* @param tokensPath on-disk `tokens.json` location.
|
|
770
809
|
* @param box at-rest `SecretBox` (encrypt-on-write / decrypt-on-read).
|
|
@@ -779,7 +818,9 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
779
818
|
*/
|
|
780
819
|
constructor(tokensPath: string, box: SecretBox, fetchImpl?: FetchLike | undefined,
|
|
781
820
|
/** Injectable, strictly read-only external CLI native-store reader. */
|
|
782
|
-
externalCliReader?: ExternalCliReader
|
|
821
|
+
externalCliReader?: ExternalCliReader,
|
|
822
|
+
/** Injectable atomic-replace seam (tests prove a failed write keeps the prior file). */
|
|
823
|
+
atomicReplace?: AtomicFileReplace);
|
|
783
824
|
/**
|
|
784
825
|
* The proxy-aware `FetchLike` for one refresh round-trip (upstream-proxy M1). A
|
|
785
826
|
* TEST-injected `fetchImpl` is returned verbatim; otherwise the refresh routes
|
|
@@ -858,13 +899,21 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
858
899
|
* destroy the ability to refresh again). HONEST `false` when no refresh_token.
|
|
859
900
|
*/
|
|
860
901
|
refreshGeminiToken(): Promise<boolean>;
|
|
902
|
+
/**
|
|
903
|
+
* Refresh the Kimi Code (Moonshot) OAuth access token (device-flow grant).
|
|
904
|
+
* Kimi ROTATES the refresh token, so the response's pair is written back
|
|
905
|
+
* whole; the account's stable `deviceId` (fingerprint header input) is
|
|
906
|
+
* preserved. The refresh call carries the CLI fingerprint headers. HONEST
|
|
907
|
+
* `false` when no refresh_token.
|
|
908
|
+
*/
|
|
909
|
+
refreshKimiToken(): Promise<boolean>;
|
|
861
910
|
/**
|
|
862
911
|
* Refresh a SPECIFIC managed account by id (background scheduler sweep and
|
|
863
912
|
* account-pool resolution). It uses only that account's stored refresh
|
|
864
913
|
* token. Coalesced per `provider:id`; on failure flags ONLY that account
|
|
865
914
|
* `expired`.
|
|
866
915
|
*/
|
|
867
|
-
refreshAccountById(provider: 'claude' | 'codex' | 'gemini', id: string): Promise<boolean>;
|
|
916
|
+
refreshAccountById(provider: 'claude' | 'codex' | 'gemini' | 'kimi', id: string): Promise<boolean>;
|
|
868
917
|
/**
|
|
869
918
|
* Resolve a SPECIFIC account's access token by id (design D6). Mirrors each
|
|
870
919
|
* provider's ACTIVE-getter policy, keyed by id: claude returns the stored token
|
|
@@ -1030,22 +1079,49 @@ declare class JsonSubscriptionCredentialStore implements SubscriptionCredentialS
|
|
|
1030
1079
|
/** Write the merged config to disk as pretty JSON (mkdir parent if needed).
|
|
1031
1080
|
* Encrypt-on-write: the token-material fields are encrypted (legacy plaintext
|
|
1032
1081
|
* `enc:v1:`; already-`enc:`/`$ENV` untouched) before serializing, so any
|
|
1033
|
-
* write incl. child 4's future refresh writes lands encrypted.
|
|
1082
|
+
* write incl. child 4's future refresh writes lands encrypted.
|
|
1083
|
+
* ATOMIC: temp + fsync + rename (`atomicReplaceUtf8`) — a failed or
|
|
1084
|
+
* interrupted write discards only the temp file; the prior `tokens.json`
|
|
1085
|
+
* survives byte-equal (bare `writeFileSync` truncate-writes lost every
|
|
1086
|
+
* account on a mid-write failure, 2026-09-06). */
|
|
1034
1087
|
private persist;
|
|
1035
1088
|
/**
|
|
1036
|
-
* Read + parse `tokens.json`,
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1089
|
+
* Read + parse `tokens.json`, then DECRYPT the token-material fields so every
|
|
1090
|
+
* getter returns plaintext (the subscription bearer path is byte-identical).
|
|
1091
|
+
*
|
|
1092
|
+
* A MISSING file is a legitimate first-boot state → minimal `{ updatedAt: '' }`.
|
|
1093
|
+
* A file that EXISTS but cannot be parsed as a JSON object is CORRUPT →
|
|
1094
|
+
* `quarantineCorrupt` moves it aside (once) before the empty config is
|
|
1095
|
+
* returned, so the unreadable accounts survive for manual recovery.
|
|
1039
1096
|
*
|
|
1040
|
-
* The
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
* `config.ts loadConfig`, which decrypts outside its parse try.
|
|
1097
|
+
* The DECRYPT runs OUTSIDE any try, so a wrong/missing master key or a
|
|
1098
|
+
* tampered `enc:` envelope FAILS FAST with the box's clear, secret-free
|
|
1099
|
+
* error (secrets spec "/ UX": SHALL fail-fast, SHALL NOT a swallowed
|
|
1100
|
+
* decrypt would report "no tokens" and silently send the WRONG bearer
|
|
1101
|
+
* upstream 401). Mirrors `config.ts loadConfig`, which decrypts outside
|
|
1102
|
+
* its parse try.
|
|
1047
1103
|
*/
|
|
1048
1104
|
private readConfig;
|
|
1105
|
+
/** One-shot latch: a corrupt file is quarantined (or found unmovable) at
|
|
1106
|
+
* most once per process, so the hot read path never re-attempts or re-logs. */
|
|
1107
|
+
private corruptQuarantined;
|
|
1108
|
+
/**
|
|
1109
|
+
* Quarantine a present-but-corrupt `tokens.json`, then treat it as empty.
|
|
1110
|
+
*
|
|
1111
|
+
* Renames the file to a sibling `tokens.json.corrupt-<stamp>` backup and
|
|
1112
|
+
* logs loudly (the daemon's stderr log; secret-free — reason + paths only).
|
|
1113
|
+
* The daemon KEEPS SERVING (API-key routing is unaffected; subscription
|
|
1114
|
+
* routing reports no credential, same as an absent file) while the corrupt
|
|
1115
|
+
* bytes survive for manual recovery — and, critically, the NEXT persist
|
|
1116
|
+
* (e.g. the user re-logging in) can no longer overwrite the only copy of
|
|
1117
|
+
* the old accounts, which is exactly how the 2026-09-06 incident turned a
|
|
1118
|
+
* recoverable truncated file into permanent account loss.
|
|
1119
|
+
*
|
|
1120
|
+
* Best-effort: if the rename fails (file locked, permissions), the corrupt
|
|
1121
|
+
* file is left in place and every later read still tolerates it as empty;
|
|
1122
|
+
* the latch still trips so the attempt + log happen exactly once.
|
|
1123
|
+
*/
|
|
1124
|
+
private quarantineCorrupt;
|
|
1049
1125
|
}
|
|
1050
1126
|
|
|
1051
1127
|
/**
|
|
@@ -1349,9 +1425,140 @@ declare class ClaudeAllowanceCollector {
|
|
|
1349
1425
|
private unsupportedSnapshot;
|
|
1350
1426
|
}
|
|
1351
1427
|
|
|
1428
|
+
/**
|
|
1429
|
+
* Codex (ChatGPT) OAuth usage collector.
|
|
1430
|
+
*
|
|
1431
|
+
* Actively polls `GET https://chatgpt.com/backend-api/wham/usage` per codex
|
|
1432
|
+
* account (Bearer + `ChatGPT-Account-Id` decoded from the OAuth id_token),
|
|
1433
|
+
* mirroring the Claude collector's cache/coalescing contract: 5-minute cache,
|
|
1434
|
+
* per-account in-flight merging, one 401→refresh→retry. The passive
|
|
1435
|
+
* `x-codex-*` response-header tap (`upstreamFetch`) remains the complement —
|
|
1436
|
+
* it keeps windows fresh mid-flight; this collector makes the quota visible
|
|
1437
|
+
* with ZERO traffic (previously codex reported not-observed until a real
|
|
1438
|
+
* model response, and its refresh button had to spend a probe request).
|
|
1439
|
+
*
|
|
1440
|
+
* `reset_at` (absolute epoch seconds) is preferred over `reset_after_seconds`
|
|
1441
|
+
* so deadlines do not accumulate observation-clock skew. Tokens and raw
|
|
1442
|
+
* upstream payloads never leave this module.
|
|
1443
|
+
*/
|
|
1444
|
+
|
|
1445
|
+
interface CodexAllowanceCredentialReader {
|
|
1446
|
+
getAccessTokenForAccount(providerId: 'codex', accountId: string): Promise<string | null>;
|
|
1447
|
+
refreshAccountToken(providerId: 'codex', accountId: string): Promise<boolean>;
|
|
1448
|
+
}
|
|
1449
|
+
type CodexAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
|
|
1450
|
+
interface CodexAllowanceCollectOptions {
|
|
1451
|
+
force?: boolean;
|
|
1452
|
+
/** Treat an otherwise valid cache entry as due when it expires within this window. */
|
|
1453
|
+
refreshAheadMs?: number;
|
|
1454
|
+
}
|
|
1455
|
+
declare class CodexAllowanceCollector {
|
|
1456
|
+
private readonly credentials;
|
|
1457
|
+
private readonly store;
|
|
1458
|
+
private readonly fetchImpl;
|
|
1459
|
+
private readonly now;
|
|
1460
|
+
private readonly inFlight;
|
|
1461
|
+
constructor(credentials: CodexAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: CodexAllowanceFetch, now?: () => number);
|
|
1462
|
+
collectMany(accounts: readonly SubscriptionAccountEntry<CodexTokenConfig>[], options?: CodexAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
|
|
1463
|
+
collect(account: SubscriptionAccountEntry<CodexTokenConfig>, options?: CodexAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
|
|
1464
|
+
/**
|
|
1465
|
+
* A response-header snapshot stays a valid cache hit only while fresh; an
|
|
1466
|
+
* active oauth-usage snapshot is honored on the same 5-minute cadence as
|
|
1467
|
+
* Claude's (the poll is cheap and quota is the scheduling input).
|
|
1468
|
+
*/
|
|
1469
|
+
private isCacheValid;
|
|
1470
|
+
private fetchAccount;
|
|
1471
|
+
private request;
|
|
1472
|
+
private failureSnapshot;
|
|
1473
|
+
private unsupportedSnapshot;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Kimi Code OAuth usage collector.
|
|
1478
|
+
*
|
|
1479
|
+
* Polls `GET https://api.kimi.com/coding/v1/usages` per kimi account (Bearer +
|
|
1480
|
+
* the CLI fingerprint headers), mirroring the Claude/Codex collectors' cache
|
|
1481
|
+
* contract: 5-minute cache, per-account in-flight merging, one 401→refresh→
|
|
1482
|
+
* retry. The payload's `usage` aggregate is a weekly row; `limits[]` carries
|
|
1483
|
+
* the per-window rows — the 300-minute burst window normalizes to `five-hour`
|
|
1484
|
+
* and whole-day spans to `seven-day` (the same canonical ids the Claude view
|
|
1485
|
+
* and the UI's window labels use).
|
|
1486
|
+
*/
|
|
1487
|
+
|
|
1488
|
+
interface KimiAllowanceCredentialReader {
|
|
1489
|
+
getAccessTokenForAccount(providerId: 'kimi', accountId: string): Promise<string | null>;
|
|
1490
|
+
refreshAccountToken(providerId: 'kimi', accountId: string): Promise<boolean>;
|
|
1491
|
+
}
|
|
1492
|
+
type KimiAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
|
|
1493
|
+
interface KimiAllowanceCollectOptions {
|
|
1494
|
+
force?: boolean;
|
|
1495
|
+
refreshAheadMs?: number;
|
|
1496
|
+
}
|
|
1497
|
+
declare class KimiAllowanceCollector {
|
|
1498
|
+
private readonly credentials;
|
|
1499
|
+
private readonly store;
|
|
1500
|
+
private readonly fetchImpl;
|
|
1501
|
+
private readonly now;
|
|
1502
|
+
private readonly inFlight;
|
|
1503
|
+
constructor(credentials: KimiAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: KimiAllowanceFetch, now?: () => number);
|
|
1504
|
+
collectMany(accounts: readonly SubscriptionAccountEntry<KimiTokenConfig>[], options?: KimiAllowanceCollectOptions): Promise<AccountAllowanceSnapshot[]>;
|
|
1505
|
+
collect(account: SubscriptionAccountEntry<KimiTokenConfig>, options?: KimiAllowanceCollectOptions): Promise<AccountAllowanceSnapshot>;
|
|
1506
|
+
private isCacheValid;
|
|
1507
|
+
private fetchAccount;
|
|
1508
|
+
private request;
|
|
1509
|
+
private failureSnapshot;
|
|
1510
|
+
private unsupportedSnapshot;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* OpenCodeGo usage collector.
|
|
1515
|
+
*
|
|
1516
|
+
* Polls `GET {go-base}/v1/usage` per OpenCodeGo account with the account's
|
|
1517
|
+
* static bearer key, mirroring the other collectors' cache contract (5-minute
|
|
1518
|
+
* cache, per-account in-flight merging). The payload reports three percent
|
|
1519
|
+
* windows; only rolling(≈5h) + weekly are surfaced — the MONTHLY window is
|
|
1520
|
+
* deliberately dropped: the console's "Use balance" fallback keeps a
|
|
1521
|
+
* monthly-exhausted key SERVING, and the scheduling policy pauses on the worst
|
|
1522
|
+
* reported window, so reporting monthly would strand usable keys (oh-my-pi
|
|
1523
|
+
* reached the same conclusion for its ranking scopes).
|
|
1524
|
+
*
|
|
1525
|
+
* `status: "rate-limited"` is authoritative over the percent (→ 100%).
|
|
1526
|
+
*/
|
|
1527
|
+
|
|
1528
|
+
interface OpenCodeGoAllowanceCredentialReader {
|
|
1529
|
+
getAccessTokenForAccount(providerId: 'opencodego', accountId: string): Promise<string | null>;
|
|
1530
|
+
}
|
|
1531
|
+
type OpenCodeGoAllowanceFetch = (url: string, init: RequestInit, accountId: string) => Promise<Response>;
|
|
1532
|
+
declare class OpenCodeGoAllowanceCollector {
|
|
1533
|
+
private readonly credentials;
|
|
1534
|
+
private readonly store;
|
|
1535
|
+
private readonly fetchImpl;
|
|
1536
|
+
private readonly now;
|
|
1537
|
+
private readonly inFlight;
|
|
1538
|
+
constructor(credentials: OpenCodeGoAllowanceCredentialReader, store?: AccountAllowanceStore, fetchImpl?: OpenCodeGoAllowanceFetch, now?: () => number);
|
|
1539
|
+
collectMany(accounts: readonly SubscriptionAccountEntry<OpenCodeGoTokenConfig>[], options?: {
|
|
1540
|
+
force?: boolean;
|
|
1541
|
+
refreshAheadMs?: number;
|
|
1542
|
+
}): Promise<AccountAllowanceSnapshot[]>;
|
|
1543
|
+
collect(account: SubscriptionAccountEntry<OpenCodeGoTokenConfig>, options?: {
|
|
1544
|
+
force?: boolean;
|
|
1545
|
+
refreshAheadMs?: number;
|
|
1546
|
+
}): Promise<AccountAllowanceSnapshot>;
|
|
1547
|
+
private fetchAccount;
|
|
1548
|
+
private failureSnapshot;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1352
1551
|
/** Secret-free account allowance query/refresh facade used by the admin API. */
|
|
1353
1552
|
|
|
1354
|
-
|
|
1553
|
+
/**
|
|
1554
|
+
* The credential surface the collectors share. Declared explicitly (not via
|
|
1555
|
+
* interface extension) because the collectors narrow
|
|
1556
|
+
* `getAccessTokenForAccount`/`refreshAccountToken` to different provider
|
|
1557
|
+
* literals — extending them all would make the overloads conflict.
|
|
1558
|
+
*/
|
|
1559
|
+
interface AccountAllowanceCredentialReader {
|
|
1560
|
+
getAccessTokenForAccount(providerId: 'claude' | 'codex' | 'kimi' | 'opencodego', accountId: string): Promise<string | null>;
|
|
1561
|
+
refreshAccountToken(providerId: 'claude' | 'codex' | 'kimi', accountId: string): Promise<boolean>;
|
|
1355
1562
|
getFullConfig(): Promise<AccountTokensConfig>;
|
|
1356
1563
|
}
|
|
1357
1564
|
interface AccountAllowanceFilter {
|
|
@@ -1367,18 +1574,35 @@ declare class AccountAllowanceService {
|
|
|
1367
1574
|
private readonly store;
|
|
1368
1575
|
private readonly now;
|
|
1369
1576
|
readonly claudeCollector: ClaudeAllowanceCollector;
|
|
1370
|
-
|
|
1577
|
+
readonly codexCollector: CodexAllowanceCollector;
|
|
1578
|
+
readonly kimiCollector: KimiAllowanceCollector;
|
|
1579
|
+
readonly opencodegoCollector: OpenCodeGoAllowanceCollector;
|
|
1580
|
+
constructor(credentials: AccountAllowanceCredentialReader, store?: AccountAllowanceStore, collector?: ClaudeAllowanceCollector, codexCollector?: CodexAllowanceCollector, kimiCollector?: KimiAllowanceCollector, opencodegoCollector?: OpenCodeGoAllowanceCollector, now?: () => number);
|
|
1371
1581
|
/**
|
|
1372
|
-
* Read all/filtered snapshots. Claude's five-minute
|
|
1373
|
-
*
|
|
1582
|
+
* Read all/filtered snapshots. Claude's and Codex's five-minute caches are
|
|
1583
|
+
* refreshed lazily on read (Codex polls `/backend-api/wham/usage`; the
|
|
1584
|
+
* passive `x-codex-*` header tap still feeds mid-flight updates).
|
|
1374
1585
|
*/
|
|
1375
1586
|
list(filter?: AccountAllowanceFilter): Promise<AccountAllowanceSnapshot[]>;
|
|
1587
|
+
private knownAccounts;
|
|
1376
1588
|
/** Force-refresh Claude usage for one account or every stored Claude account. */
|
|
1377
1589
|
refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
1378
1590
|
/**
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1591
|
+
* Force-refresh Codex usage (`/backend-api/wham/usage`) for one account or
|
|
1592
|
+
* every stored Codex account. Replaces the old probe-request workaround —
|
|
1593
|
+
* no quota is spent reading the usage endpoint.
|
|
1594
|
+
*/
|
|
1595
|
+
refreshCodex(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
1596
|
+
/** Force-refresh OpenCodeGo usage (`{go}/v1/usage`) for one/all accounts. */
|
|
1597
|
+
refreshOpenCodeGo(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
1598
|
+
/** Force-refresh Kimi usage (`/coding/v1/usages`) for one/all accounts. */
|
|
1599
|
+
refreshKimi(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
1600
|
+
/**
|
|
1601
|
+
* Keep Claude + Codex + Kimi snapshots warm for allowance-aware routing. All
|
|
1602
|
+
* collectors preserve their cache + per-account in-flight coalescing; a tick
|
|
1603
|
+
* normally performs no network I/O. (Codex joined the warm path when it
|
|
1604
|
+
* gained an active `/wham/usage` collector — the passive `x-codex-*` header
|
|
1605
|
+
* tap alone could not keep the policy fed while idle.)
|
|
1382
1606
|
*/
|
|
1383
1607
|
maintainClaudeCache(refreshAheadMs: number): Promise<void>;
|
|
1384
1608
|
/** Remove a cache row as soon as an account is deleted by the admin path. */
|
|
@@ -1743,6 +1967,62 @@ type AuditCompactor = () => {
|
|
|
1743
1967
|
/** The read surface the AdminServer consumes (bootstrap binds it to the ledger dir). */
|
|
1744
1968
|
type BillingStatusReader = () => BillingDeliveryStatus;
|
|
1745
1969
|
|
|
1970
|
+
/**
|
|
1971
|
+
* ProviderKeyQuota — BYO provider-row key quota parsing (pure functions).
|
|
1972
|
+
*
|
|
1973
|
+
* Subscription accounts have the daemon's allowance collectors; BYO rows (API
|
|
1974
|
+
* keys pasted from a provider console) had NO quota surface at all. Several CN
|
|
1975
|
+
* coding-plan providers expose a same-key usage endpoint:
|
|
1976
|
+
*
|
|
1977
|
+
* - Z.AI / Zhipu bigmodel (GLM Coding Plan):
|
|
1978
|
+
* GET {origin}/api/monitor/usage/quota/limit
|
|
1979
|
+
* Raw `Authorization: <key>` (NO Bearer prefix). Envelope
|
|
1980
|
+
* `{success, data: {limits[], level}}`; each limit carries
|
|
1981
|
+
* `{type, usage(limit), currentValue(used), percentage, remaining,
|
|
1982
|
+
* nextResetTime, unit(3=h/4=d/5=mo/6=w), number, usageDetails[]}`.
|
|
1983
|
+
* A coding-plan key reports 5h + weekly credit windows; a PAYG key's shape
|
|
1984
|
+
* is unknown → defensive parse, unavailable on surprise.
|
|
1985
|
+
* - MiniMax Token Plan:
|
|
1986
|
+
* GET {origin}/v1/token_plan/remains
|
|
1987
|
+
* `Authorization: Bearer <key>`. HTTP is ALWAYS 200 — `base_resp
|
|
1988
|
+
* .status_code === 0` is the real success gate. `model_remains[]` buckets
|
|
1989
|
+
* each carry a rolling interval + weekly window as REMAINING percent
|
|
1990
|
+
* (0-100); the `"general"` bucket is the plan-wide shared quota.
|
|
1991
|
+
*
|
|
1992
|
+
* Everything here is pure; the fetch/cache lifecycle lives in
|
|
1993
|
+
* `ProviderKeyQuotaService`. Windows reuse the subscription `AllowanceWindow`
|
|
1994
|
+
* DTO so the UI renders one shape.
|
|
1995
|
+
*/
|
|
1996
|
+
|
|
1997
|
+
/** Which quota adapter applies to a provider row (by resolved endpoint). */
|
|
1998
|
+
type ProviderKeyQuotaAdapter = 'zai' | 'minimax-token-plan' | 'umans' | 'synthetic';
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* ProviderKeyQuotaService — read-through quota cache for BYO provider-row keys.
|
|
2002
|
+
*
|
|
2003
|
+
* The UI polls `GET /admin/api/providers/:id/keys` every few seconds for pool
|
|
2004
|
+
* health; this service backs the optional `quota` field on that DTO. A read is
|
|
2005
|
+
* cache-first (5-minute TTL) with per-key in-flight coalescing, so the UI's poll
|
|
2006
|
+
* cadence never translates into upstream request cadence. `force` (the refresh
|
|
2007
|
+
* button) bypasses the cache. Failures degrade to a stale marker — quota
|
|
2008
|
+
* telemetry must never break the keys view.
|
|
2009
|
+
*
|
|
2010
|
+
* The key plaintext is resolved from the live row (same synthesis as the pool
|
|
2011
|
+
* loader: explicit `apiKeys[]` else the single-key fallback) and decrypted via
|
|
2012
|
+
* the injected box; it is used ONLY for the upstream Authorization header and
|
|
2013
|
+
* never appears in any returned DTO.
|
|
2014
|
+
*/
|
|
2015
|
+
|
|
2016
|
+
/** Secret-free quota view for one pool key. */
|
|
2017
|
+
interface ProviderKeyQuota {
|
|
2018
|
+
adapter: ProviderKeyQuotaAdapter;
|
|
2019
|
+
observedAt: string;
|
|
2020
|
+
expiresAt: string;
|
|
2021
|
+
windows: AllowanceWindow[];
|
|
2022
|
+
/** Stable display-safe diagnostic code on a failed probe. */
|
|
2023
|
+
errorCode?: string;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1746
2026
|
type PreparedImageRuntimeGeneration = {
|
|
1747
2027
|
readonly id: string;
|
|
1748
2028
|
readonly enabled: true;
|
|
@@ -2511,6 +2791,12 @@ interface AccountAllowanceAdminReader {
|
|
|
2511
2791
|
accountId?: string;
|
|
2512
2792
|
}): Promise<AccountAllowanceSnapshot[]>;
|
|
2513
2793
|
refreshClaude(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
2794
|
+
/** Optional: Codex active `/wham/usage` refresh (absent on older daemons). */
|
|
2795
|
+
refreshCodex?(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
2796
|
+
/** Optional: Kimi `/coding/v1/usages` refresh (absent on older daemons). */
|
|
2797
|
+
refreshKimi?(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
2798
|
+
/** Optional: OpenCodeGo `/v1/usage` refresh (absent on older daemons). */
|
|
2799
|
+
refreshOpenCodeGo?(accountId?: string): Promise<AccountAllowanceSnapshot[]>;
|
|
2514
2800
|
removeAccountSnapshot?(providerId: SubscriptionProviderId, accountId: string): void;
|
|
2515
2801
|
removeProviderSnapshots?(providerId: SubscriptionProviderId): void;
|
|
2516
2802
|
getSchedulingStatus?(): AccountAllowanceSchedulingStatus;
|
|
@@ -2535,6 +2821,16 @@ interface PoolKeyHealth {
|
|
|
2535
2821
|
interface PoolHealthReader {
|
|
2536
2822
|
getKeyHealth(providerId: string): Promise<Record<string, PoolKeyHealth>>;
|
|
2537
2823
|
}
|
|
2824
|
+
/**
|
|
2825
|
+
* The BYO provider-key quota surface the keys view needs — structurally
|
|
2826
|
+
* satisfied by `ProviderKeyQuotaService`. Read-only; the DTO is secret-free by
|
|
2827
|
+
* construction (normalized windows + diagnostic codes only).
|
|
2828
|
+
*/
|
|
2829
|
+
interface ProviderKeyQuotaReader {
|
|
2830
|
+
quotaFor(row: DaemonProviderConfig, keyId: string, options?: {
|
|
2831
|
+
force?: boolean;
|
|
2832
|
+
}): Promise<ProviderKeyQuota | null>;
|
|
2833
|
+
}
|
|
2538
2834
|
interface AdminImagesStatusReader {
|
|
2539
2835
|
inspectCapability(apiKeyId: string): Promise<ImageRuntimeCapabilityInspection>;
|
|
2540
2836
|
status(): ImageRuntimeManagerStatus;
|
|
@@ -2612,6 +2908,12 @@ interface AdminApiDeps {
|
|
|
2612
2908
|
readonly apiKeyPool: PoolHealthReader;
|
|
2613
2909
|
/** In-memory auto-disable store (design D5) — read-only for the health view. */
|
|
2614
2910
|
readonly autoDisableStore: AutoDisableStore;
|
|
2911
|
+
/**
|
|
2912
|
+
* OPTIONAL BYO provider-key quota service — same-key usage/quota probes for
|
|
2913
|
+
* provider rows with a known adapter (Z.AI coding plan, MiniMax Token Plan).
|
|
2914
|
+
* Absent ⇒ the keys view carries no `quota` field (light embedders).
|
|
2915
|
+
*/
|
|
2916
|
+
readonly providerKeyQuota?: ProviderKeyQuotaReader;
|
|
2615
2917
|
/**
|
|
2616
2918
|
* Pending interactive-OAuth sessions (app-parity child 4, design D1) — the
|
|
2617
2919
|
* in-memory `{ codeVerifier, state }` map keyed by a minted `sessionId`,
|
|
@@ -2639,6 +2941,11 @@ interface AdminApiDeps {
|
|
|
2639
2941
|
* flight (port 1455 is one resource). Wired in `bootstrap.ts`.
|
|
2640
2942
|
*/
|
|
2641
2943
|
readonly codexSessions: CodexOAuthSessionStore;
|
|
2944
|
+
/**
|
|
2945
|
+
* Kimi interactive-OAuth flow store (device code). Same token-free polled
|
|
2946
|
+
* shape as codex; one sign-in at a time. Wired in `bootstrap.ts`.
|
|
2947
|
+
*/
|
|
2948
|
+
readonly kimiSessions: CodexOAuthSessionStore;
|
|
2642
2949
|
/**
|
|
2643
2950
|
* Codex loopback listener (app-parity-2 child 5) — defaults to `awaitLoopbackCode`
|
|
2644
2951
|
* (binds 127.0.0.1:1455) in `bootstrap.ts`; tests inject a mock so no real port
|
|
@@ -3467,7 +3774,6 @@ declare function createImageRuntimeGeneration(options: ImageRuntimeGenerationFac
|
|
|
3467
3774
|
* @module @omnicross/daemon/ports/JsonOutboundKeyDb
|
|
3468
3775
|
*/
|
|
3469
3776
|
|
|
3470
|
-
type AtomicFileReplace = (targetPath: string, contents: string) => void;
|
|
3471
3777
|
declare class JsonOutboundKeyDb implements OutboundKeyDb {
|
|
3472
3778
|
private readonly keysPath;
|
|
3473
3779
|
private readonly secretBox?;
|