@omnicross/daemon 0.3.0 → 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/dist/index.d.cts 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`, tolerating a missing/corrupt
714
- * file by returning a minimal `{ updatedAt }` config (the strategies already
715
- * guard `?.accessToken`, so a partial/empty config never crashes dispatch).
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`, tolerating a missing/corrupt file, then DECRYPT
1037
- * the token-material fields so every getter returns plaintext (the
1038
- * subscription bearer path is byte-identical).
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 fs-read + JSON-parse tolerance is INSIDE the try (a missing or corrupt
1041
- * file empty `{ updatedAt: '' }`). The DECRYPT runs OUTSIDE the try, so a
1042
- * wrong/missing master key or a tampered `enc:` envelope FAILS FAST with the
1043
- * box's clear, secret-free error (secrets spec "/ UX":
1044
- * SHALL fail-fast, SHALL NOT a swallowed decrypt would report "no
1045
- * tokens" and silently send the WRONG bearer upstream 401). Mirrors
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
- interface AccountAllowanceCredentialReader extends ClaudeAllowanceCredentialReader {
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
- constructor(credentials: AccountAllowanceCredentialReader, store?: AccountAllowanceStore, collector?: ClaudeAllowanceCollector, now?: () => number);
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 cache is refreshed lazily;
1373
- * Codex remains passive and reports not-observed until a real model response.
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
- * Keep Claude snapshots warm for allowance-aware routing. This deliberately
1380
- * excludes Codex (whose quota is learned from real response headers) and
1381
- * preserves the collector's cache + per-account in-flight coalescing.
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?;