@eddyskywalker/dsh-chatgpt-subscription 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +2 -0
  3. package/lib/client.js +190 -11
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +570 -19
  6. package/lib/types/client/antigravity/AntigravitySection.d.ts.map +1 -1
  7. package/lib/types/client/antigravity/locales.d.ts +47 -2
  8. package/lib/types/client/antigravity/locales.d.ts.map +1 -1
  9. package/lib/types/client/kimi-code/locales.d.ts +2 -2
  10. package/lib/types/client/workbuddy/WorkBuddySection.d.ts +36 -0
  11. package/lib/types/client/workbuddy/WorkBuddySection.d.ts.map +1 -1
  12. package/lib/types/client/workbuddy/locales.d.ts +18 -0
  13. package/lib/types/client/workbuddy/locales.d.ts.map +1 -1
  14. package/lib/types/host/antigravity/account-pool.d.ts +24 -0
  15. package/lib/types/host/antigravity/account-pool.d.ts.map +1 -1
  16. package/lib/types/host/antigravity/adapter.d.ts.map +1 -1
  17. package/lib/types/host/antigravity/cache-stats.d.ts +166 -0
  18. package/lib/types/host/antigravity/cache-stats.d.ts.map +1 -0
  19. package/lib/types/host/antigravity/mapper.d.ts +75 -3
  20. package/lib/types/host/antigravity/mapper.d.ts.map +1 -1
  21. package/lib/types/host/antigravity/routes.d.ts +1 -1
  22. package/lib/types/host/antigravity/routes.d.ts.map +1 -1
  23. package/lib/types/host/workbuddy/client.d.ts +8 -0
  24. package/lib/types/host/workbuddy/client.d.ts.map +1 -1
  25. package/lib/types/host/workbuddy/model-catalog.d.ts +30 -1
  26. package/lib/types/host/workbuddy/model-catalog.d.ts.map +1 -1
  27. package/lib/types/host/workbuddy/routes.d.ts +7 -0
  28. package/lib/types/host/workbuddy/routes.d.ts.map +1 -1
  29. package/lib/types/shared/antigravity-contracts.d.ts +76 -0
  30. package/lib/types/shared/antigravity-contracts.d.ts.map +1 -1
  31. package/lib/types/shared/workbuddy-contracts.d.ts +42 -0
  32. package/lib/types/shared/workbuddy-contracts.d.ts.map +1 -1
  33. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -9812,14 +9812,80 @@ const OMITTED_IMAGE_TEXT$4 = "[image omitted to keep the request within its imag
9812
9812
  * instruction, the conversation text, and the tool declarations.
9813
9813
  */
9814
9814
  const MAX_REQUEST_IMAGE_BYTES$4 = 12 * 1024 * 1024;
9815
+ /**
9816
+ * Longest edge, in pixels, one image of this request may keep.
9817
+ *
9818
+ * CHOICE: 1024 px matches the long edge the other scaled routes send, and it is
9819
+ * what makes the scaling in {@link resolveRequestImages} effective rather than
9820
+ * nominal: the target is computed from the stored image's own dimensions, so the
9821
+ * same image yields the same request bytes in every later turn and the prefix
9822
+ * Google's implicit cache matches stays byte-stable.
9823
+ *
9824
+ * The budget stays Antigravity's own 12 MB. The 1.5 MB in
9825
+ * host/common/request-images.ts is Kimi's, and that file warns in the same
9826
+ * breath that importing one route's number into another refuses the other
9827
+ * route's own legal images; Google accepts a request of up to 20 MB.
9828
+ */
9829
+ const REQUEST_IMAGE_MAX_EDGE$1 = 1024;
9830
+ /**
9831
+ * Encoded-byte target for one downscaled request version.
9832
+ *
9833
+ * 256 KiB of raw bytes is about 350 KiB of base64, so twenty of them still fit
9834
+ * the 12 MB budget with room left for the system instruction, the conversation
9835
+ * text, and the tool declarations that share the body. The harness keeps its
9836
+ * smallest output when no quality level meets the target.
9837
+ */
9838
+ const REQUEST_IMAGE_VERSION_MAX_BYTES$2 = 256 * 1024;
9839
+ /**
9840
+ * Request-version target for one stored image whose long edge is over the
9841
+ * ceiling, or undefined when it already fits and is sent as stored.
9842
+ *
9843
+ * A reference without usable dimensions is sent as stored: there is no basis for
9844
+ * choosing a target, and the budget fallback in
9845
+ * {@link offloadOldestRequestImages} still applies to it.
9846
+ */
9847
+ function requestImageTarget$1(ref, edge = REQUEST_IMAGE_MAX_EDGE$1) {
9848
+ const { width, height } = ref;
9849
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return void 0;
9850
+ if (Math.max(width, height) <= edge) return void 0;
9851
+ return width >= height ? {
9852
+ width: edge,
9853
+ height: Math.max(1, Math.round(edge * height / width)),
9854
+ maxBytes: REQUEST_IMAGE_VERSION_MAX_BYTES$2
9855
+ } : {
9856
+ width: Math.max(1, Math.round(edge * width / height)),
9857
+ height: edge,
9858
+ maxBytes: REQUEST_IMAGE_VERSION_MAX_BYTES$2
9859
+ };
9860
+ }
9815
9861
  /** Base64 length of raw image bytes, including padding. */
9816
9862
  function base64Length$4(bytes) {
9817
9863
  return Math.ceil(bytes / 3) * 4;
9818
9864
  }
9819
- /** Request payload one inline image block occupies, when it can be measured. */
9865
+ /**
9866
+ * Base64 payload one image block will actually contribute to the wire, or
9867
+ * undefined when it contributes none.
9868
+ *
9869
+ * This is the length of what is SENT, which is not the length of what is
9870
+ * STORED. An image whose long edge is over {@link REQUEST_IMAGE_MAX_EDGE} is
9871
+ * sent as its downscaled request version (see {@link resolveRequestImages}), so
9872
+ * measuring `attachment.bytes` counts a 5 MB screenshot as 5 MB when it reaches
9873
+ * the wire as a few hundred kilobytes. That over-count is not harmless: it makes
9874
+ * {@link offloadOldestRequestImages} replace an image with placeholder text that
9875
+ * would have fitted once scaled, and every replacement rewrites the prefix
9876
+ * Google's implicit cache matches.
9877
+ *
9878
+ * The exact size is only known once the attachment service has derived the
9879
+ * version, which happens after this budget runs, so an image with a scale target
9880
+ * is measured at its target ceiling. An image without one travels as stored, and
9881
+ * there the stored size is exact.
9882
+ */
9820
9883
  function requestImageBytes$3(block) {
9821
9884
  const attachment = attachmentOf$5(block);
9822
- if (attachment) return base64Length$4(attachment.bytes);
9885
+ if (attachment) {
9886
+ const target = requestImageTarget$1(attachment);
9887
+ return target ? base64Length$4(Math.min(attachment.bytes, target.maxBytes)) : base64Length$4(attachment.bytes);
9888
+ }
9823
9889
  const inline = asString$17(block.data) || asString$17(block.base64);
9824
9890
  return inline ? inline.length : void 0;
9825
9891
  }
@@ -9841,6 +9907,12 @@ function collectRequestImageBytes$3(content, lengths) {
9841
9907
  * order them, and the placeholder tells the model the image is missing instead
9842
9908
  * of letting it answer as though the picture were simply blank.
9843
9909
  *
9910
+ * Each occurrence is measured at the size it will actually be sent at, so an
9911
+ * image that only fits after scaling is not replaced (see
9912
+ * {@link requestImageBytes}). Replacing an image is the LAST resort: it rewrites
9913
+ * the retained prefix and costs the next turn its whole implicit cache hit, so
9914
+ * it is worth keeping the image count stable for as long as the bytes allow.
9915
+ *
9844
9916
  * @param options - the request about to be built; durable history stays untouched.
9845
9917
  * @returns the original options when they already fit, otherwise shallow copies.
9846
9918
  */
@@ -9890,6 +9962,17 @@ function offloadOldestRequestImages$4(options) {
9890
9962
  * model a text marker, because a turn that silently loses its image is far
9891
9963
  * harder to diagnose than one that says so.
9892
9964
  *
9965
+ * WHY SCALE INSTEAD OF DROP. `offloadOldestRequestImages` has to replace an
9966
+ * image with placeholder text once the request exceeds
9967
+ * {@link MAX_REQUEST_IMAGE_BYTES}, and the count it replaces grows with every
9968
+ * image added afterwards. Each change rewrites the retained prefix, so Google's
9969
+ * implicit cache misses again on the very next turn. An image whose long edge is
9970
+ * over {@link REQUEST_IMAGE_MAX_EDGE} is therefore sent as the harness's
9971
+ * downscaled request version, which keeps the image COUNT steady and with it the
9972
+ * prefix. The stored image and durable history do not change, and an image that
9973
+ * already fits is sent byte-for-byte as stored. The drop path stays as the last
9974
+ * resort for a request that is over budget even after scaling.
9975
+ *
9893
9976
  * @param options - the exact request about to be built.
9894
9977
  * @param attachments - durable attachment store; absent when the host wired none.
9895
9978
  * @param signal - cancellation, forwarded to every attachment read.
@@ -9906,6 +9989,16 @@ async function resolveRequestImages$4(options, attachments, signal) {
9906
9989
  return;
9907
9990
  }
9908
9991
  try {
9992
+ const target = requestImageTarget$1(ref);
9993
+ if (target !== void 0) {
9994
+ const version = await attachments.readImageRequest(ref, target, signal);
9995
+ resolved.set(attachmentId, Math.max(version.width, version.height) > 1024 ? { kind: "unavailable" } : {
9996
+ kind: "inline",
9997
+ mediaType: version.mediaType,
9998
+ data: Buffer.from(version.data).toString("base64")
9999
+ });
10000
+ return;
10001
+ }
9909
10002
  const stored = await attachments.readImage(ref, signal);
9910
10003
  resolved.set(attachmentId, {
9911
10004
  kind: "inline",
@@ -10012,7 +10105,54 @@ function replayPart(part) {
10012
10105
  if (typeof copy.text === "string") copy.text = sanitizeText$5(copy.text);
10013
10106
  return copy;
10014
10107
  }
10015
- /** Claude requires a signature on each thinking block, not a separate SSE part. */
10108
+ /**
10109
+ * Whether one part is nothing but visible text.
10110
+ *
10111
+ * A part carrying any second key keeps its own boundary. A signature, an opaque
10112
+ * provider field, `thought: true`, and a tool call all make the boundary around
10113
+ * them meaningful, and Google's implicit cache matches a byte-stable prefix, so
10114
+ * none of them may be crossed by a merge.
10115
+ */
10116
+ function isPureTextPart(part) {
10117
+ return part !== void 0 && typeof part.text === "string" && Object.keys(part).length === 1;
10118
+ }
10119
+ /**
10120
+ * Collapse each run of adjacent pure-text parts into a single part.
10121
+ *
10122
+ * SSE transport chunks are not semantic content blocks. One answer arrives as
10123
+ * hundreds of text deltas, and replaying each delta as its own part adds one
10124
+ * part per delta to the request prefix on every later turn of the conversation.
10125
+ * Antigravity relies on Google's implicit cache, which matches a byte-stable
10126
+ * prefix: the moment the prefix gains a part the previous request did not have,
10127
+ * everything after it misses. The Claude route lost 68.84% cache hits to exactly
10128
+ * that growth before its deltas were merged; the Gemini route sends the same
10129
+ * deltas and needs the same treatment.
10130
+ *
10131
+ * Merging is restricted to pure text, so signed, opaque, thinking, and tool
10132
+ * parts keep their boundaries exactly where the model produced them.
10133
+ *
10134
+ * @param parts - replay parts in model order; the input is never mutated.
10135
+ * @returns parts with every adjacent pure-text run merged into its first member.
10136
+ */
10137
+ function mergeAdjacentTextParts(parts) {
10138
+ const merged = [];
10139
+ for (const part of parts) {
10140
+ const previous = merged[merged.length - 1];
10141
+ if (isPureTextPart(part) && isPureTextPart(previous)) {
10142
+ previous.text = String(previous.text) + part.text;
10143
+ continue;
10144
+ }
10145
+ merged.push({ ...part });
10146
+ }
10147
+ return merged;
10148
+ }
10149
+ /**
10150
+ * Claude requires a signature on each thinking block, not a separate SSE part.
10151
+ *
10152
+ * The thinking/signature pairing below is Claude's wire contract alone: Vertex
10153
+ * Anthropic rejects a thinking block without its signature, while Gemini has no
10154
+ * such rule and must receive the thinking parts the model actually produced.
10155
+ */
10016
10156
  function claudeReplayParts(parts) {
10017
10157
  const result = [];
10018
10158
  let pending = "";
@@ -10040,14 +10180,9 @@ function claudeReplayParts(parts) {
10040
10180
  continue;
10041
10181
  }
10042
10182
  pending = "";
10043
- if (part.thought !== true) {
10044
- const previous = result[result.length - 1];
10045
- const pureText = (value) => value !== void 0 && typeof value.text === "string" && Object.keys(value).length === 1;
10046
- if (pureText(part) && pureText(previous)) previous.text = String(previous.text) + part.text;
10047
- else result.push({ ...part });
10048
- }
10183
+ if (part.thought !== true) result.push({ ...part });
10049
10184
  }
10050
- return result;
10185
+ return mergeAdjacentTextParts(result);
10051
10186
  }
10052
10187
  function assistantParts(message, model, runtimeModel, toolCalls) {
10053
10188
  const parts = [];
@@ -10096,7 +10231,7 @@ function assistantParts(message, model, runtimeModel, toolCalls) {
10096
10231
  parts.push(...originalParts.filter((part) => !part.functionCall).map(replayPart));
10097
10232
  }
10098
10233
  }
10099
- return runtimeModel.startsWith("claude-") ? claudeReplayParts(parts) : parts;
10234
+ return runtimeModel.startsWith("claude-") ? claudeReplayParts(parts) : mergeAdjacentTextParts(parts);
10100
10235
  }
10101
10236
  function pushToolResult(contents, result, toolCalls, model, runtimeModel, images = NO_RESOLVED_IMAGES$5) {
10102
10237
  const toolCallId = String(result.toolCallId || "");
@@ -10444,14 +10579,276 @@ function closeStream$5(state) {
10444
10579
  return out;
10445
10580
  }
10446
10581
  //#endregion
10582
+ //#region src/host/antigravity/cache-stats.ts
10583
+ const EMPTY_TOTALS = {
10584
+ requests: 0,
10585
+ cachedTokens: 0,
10586
+ freshTokens: 0
10587
+ };
10588
+ const totalsByScope = /* @__PURE__ */ new Map();
10589
+ const prefixesBySession = /* @__PURE__ */ new Map();
10590
+ const pendingBySession = /* @__PURE__ */ new Map();
10591
+ const missBySession = /* @__PURE__ */ new Map();
10592
+ /** When each session last SENT a request, for the idle reading a miss reports. */
10593
+ const requestAtBySession = /* @__PURE__ */ new Map();
10594
+ /**
10595
+ * Sessions the process is willing to remember before evicting the oldest.
10596
+ *
10597
+ * DSH runs several sessions and subagents through one host, but not hundreds at
10598
+ * once: eviction only begins once a host has served that many, and it drops the
10599
+ * least recently written key, which is a stale session by then. A purely
10600
+ * advisory feature must not be a leak.
10601
+ */
10602
+ const MAX_TRACKED_SCOPES = 256;
10603
+ /**
10604
+ * Bucket key for a request that names no session.
10605
+ *
10606
+ * A one-shot request — a probe, a test, a caller that never had a conversation —
10607
+ * is still counted, but kept apart from every real session so it can never
10608
+ * inflate, or be mistaken for, one of their ratios.
10609
+ */
10610
+ const UNSCOPED_KEY = "";
10611
+ /** The session a request belongs to, or the unscoped bucket when it names none. */
10612
+ function sessionScopeKey(sessionId) {
10613
+ return typeof sessionId === "string" && sessionId.trim() !== "" ? sessionId.trim() : UNSCOPED_KEY;
10614
+ }
10615
+ /**
10616
+ * Composite key separating one session's numbers from another's, and one
10617
+ * account's share of a session from another's.
10618
+ *
10619
+ * The account is part of the key because the pool can answer the same session
10620
+ * from different accounts, and each of those probably keeps its own cache
10621
+ * server-side. A shared entry would blend a cold account's misses into a warm
10622
+ * account's ratio, leaving a number that means nothing.
10623
+ *
10624
+ * That each account is its own cache namespace is INFERRED, not measured: no
10625
+ * account-scoped cache metric exists, and an account-scoped read is unobservable
10626
+ * from here. Splitting by account is the conservative reading of that assumption
10627
+ * — if it is wrong the two entries are merely reported apart, whereas merging
10628
+ * them would silently average a claim this line cannot check.
10629
+ */
10630
+ function statsScopeKey(sessionId, accountId) {
10631
+ const session = sessionScopeKey(sessionId);
10632
+ if (session === UNSCOPED_KEY) return UNSCOPED_KEY;
10633
+ return accountId === void 0 || accountId === "" ? session : `${session} ${accountId}`;
10634
+ }
10635
+ /** Write a key, evicting the oldest entry once the bound is reached. */
10636
+ function rememberScoped$1(store, key, value) {
10637
+ store.delete(key);
10638
+ store.set(key, value);
10639
+ if (store.size <= MAX_TRACKED_SCOPES) return;
10640
+ const oldest = store.keys().next();
10641
+ if (oldest.done !== true) store.delete(oldest.value);
10642
+ }
10643
+ /**
10644
+ * A short, stable fingerprint of one prefix segment.
10645
+ *
10646
+ * Hashed rather than stored verbatim: these strings are the whole conversation
10647
+ * on a long turn, and a 32-bit FNV-1a keeps the diagnostic dependency-free.
10648
+ * Collisions only ever cost a missed attribution, never a wrong request.
10649
+ */
10650
+ function fingerprint$1(text) {
10651
+ let hash = 2166136261;
10652
+ for (let index = 0; index < text.length; index++) {
10653
+ hash ^= text.charCodeAt(index);
10654
+ hash = Math.imul(hash, 16777619);
10655
+ }
10656
+ return (hash >>> 0).toString(16);
10657
+ }
10658
+ /**
10659
+ * One value's canonical text, with object keys in a fixed order.
10660
+ *
10661
+ * Key order is an artifact of how a value was assembled, not of what it means:
10662
+ * hashing `JSON.stringify` output verbatim would report a drift whenever two
10663
+ * structurally equal requests happened to build their objects in a different
10664
+ * order, and a reader cannot tell that false positive from a real one.
10665
+ */
10666
+ function stableSerialize(value) {
10667
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
10668
+ if (Array.isArray(value)) return `[${value.map(stableSerialize).join(",")}]`;
10669
+ const record = value;
10670
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key])}`).join(",")}}`;
10671
+ }
10672
+ /** Hash one segment, or null when the request does not carry it at all. */
10673
+ function segmentHash(value) {
10674
+ return value === void 0 ? null : fingerprint$1(stableSerialize(value));
10675
+ }
10676
+ /** The `request` object of a serialized Antigravity body, when it has one. */
10677
+ function requestSegment(body) {
10678
+ try {
10679
+ const parsed = JSON.parse(body);
10680
+ if (typeof parsed !== "object" || parsed === null) return void 0;
10681
+ const request = parsed.request;
10682
+ return typeof request === "object" && request !== null ? request : void 0;
10683
+ } catch {
10684
+ return;
10685
+ }
10686
+ }
10687
+ /** Which segment changed, or that there was nothing to compare against. */
10688
+ function driftCause(previous, current) {
10689
+ if (previous === void 0) return "new-session";
10690
+ if (previous.sessionId !== current.sessionId) return "session-id";
10691
+ if (previous.contents !== current.contents) return "contents";
10692
+ if (previous.systemInstruction !== current.systemInstruction) return "systemInstruction";
10693
+ if (previous.tools !== current.tools) return "tools";
10694
+ return "none";
10695
+ }
10696
+ /**
10697
+ * Fingerprint the prefix of a request about to be sent, and attribute whatever
10698
+ * changed since this session's previous request.
10699
+ *
10700
+ * Called once per turn, before the request goes out, so the answer describes the
10701
+ * prefix the service will actually see. The reason is remembered for the session
10702
+ * as well, so the route can publish the attribution beside the ratio it explains.
10703
+ *
10704
+ * @param body - The serialized request body, exactly as it will be sent.
10705
+ * @param sessionId - Conversation the request belongs to, when it has one.
10706
+ * @returns The change attributed to this request.
10707
+ */
10708
+ function recordCacheRequest(body, sessionId) {
10709
+ const scope = sessionScopeKey(sessionId);
10710
+ const request = requestSegment(body);
10711
+ const snapshot = {
10712
+ sessionId: typeof sessionId === "string" ? sessionId : "",
10713
+ contents: segmentHash(request?.contents),
10714
+ systemInstruction: segmentHash(request?.systemInstruction),
10715
+ tools: segmentHash(request?.tools)
10716
+ };
10717
+ const previous = prefixesBySession.get(scope);
10718
+ rememberScoped$1(prefixesBySession, scope, snapshot);
10719
+ const cause = driftCause(previous, snapshot);
10720
+ const previousRequestAt = requestAtBySession.get(scope);
10721
+ rememberScoped$1(requestAtBySession, scope, Date.now());
10722
+ const idleMs = previousRequestAt === void 0 ? void 0 : Math.max(0, Date.now() - previousRequestAt);
10723
+ rememberScoped$1(pendingBySession, scope, {
10724
+ cause,
10725
+ ...idleMs === void 0 ? {} : { idleMs }
10726
+ });
10727
+ return cause;
10728
+ }
10729
+ /** A token count worth recording, or undefined when it is not a count at all. */
10730
+ function usableCount(value) {
10731
+ return Number.isFinite(value) && value >= 0 ? value : void 0;
10732
+ }
10733
+ /**
10734
+ * Add one request's reported counts to its session's totals.
10735
+ *
10736
+ * The scope carries the account as well as the session, because a pooled line
10737
+ * can answer one conversation from several accounts and each of those keeps its
10738
+ * own cache server-side: one account's cold misses must not be averaged into
10739
+ * another's warm hits.
10740
+ *
10741
+ * @param sample - Token counts the response reported.
10742
+ * @param sessionId - Conversation the request belonged to.
10743
+ * @param accountId - Pooled account that served it, when the pool is in use.
10744
+ */
10745
+ function recordCacheUsage(sample, sessionId, accountId) {
10746
+ const cached = usableCount(sample.cachedTokens);
10747
+ const fresh = usableCount(sample.freshTokens);
10748
+ if (cached === void 0 || fresh === void 0) return;
10749
+ const scope = statsScopeKey(sessionId, accountId);
10750
+ const previous = totalsByScope.get(scope) ?? EMPTY_TOTALS;
10751
+ rememberScoped$1(totalsByScope, scope, {
10752
+ requests: previous.requests + 1,
10753
+ cachedTokens: previous.cachedTokens + cached,
10754
+ freshTokens: previous.freshTokens + fresh
10755
+ });
10756
+ const session = sessionScopeKey(sessionId);
10757
+ const pending = pendingBySession.get(session);
10758
+ if (cached !== 0 || pending === void 0) return;
10759
+ rememberScoped$1(missBySession, session, {
10760
+ cause: pending.cause,
10761
+ ...pending.idleMs === void 0 ? {} : { idleMs: pending.idleMs },
10762
+ at: Date.now()
10763
+ });
10764
+ }
10765
+ /** Add one scope's totals into an accumulator. */
10766
+ function accumulate$1(totals, stats) {
10767
+ totals.requests += stats.requests;
10768
+ totals.cachedTokens += stats.cachedTokens;
10769
+ totals.freshTokens += stats.freshTokens;
10770
+ }
10771
+ /**
10772
+ * Sum the scopes one question covers.
10773
+ *
10774
+ * The three cases are not a convenience: naming an account answers "is this
10775
+ * account's cache warm", naming only a session answers "how is this
10776
+ * conversation doing overall", and naming neither answers "how is this install
10777
+ * doing". Summing the stored scopes rather than keeping a second running total
10778
+ * means the aggregate can never drift out of step with the parts it summarises.
10779
+ */
10780
+ function sumTotals(sessionId, accountId) {
10781
+ const totals = { ...EMPTY_TOTALS };
10782
+ if (sessionId === void 0) for (const stats of totalsByScope.values()) accumulate$1(totals, stats);
10783
+ else if (accountId !== void 0) accumulate$1(totals, totalsByScope.get(statsScopeKey(sessionId, accountId)) ?? EMPTY_TOTALS);
10784
+ else {
10785
+ const scope = sessionScopeKey(sessionId);
10786
+ const prefix = scope + " ";
10787
+ for (const [key, stats] of totalsByScope) if (key === scope || key.startsWith(prefix)) accumulate$1(totals, stats);
10788
+ }
10789
+ return totals;
10790
+ }
10791
+ /** The most recent miss for the requested scope, when one was recorded. */
10792
+ function missFor(sessionId) {
10793
+ if (sessionId === void 0) {
10794
+ let latest;
10795
+ for (const record of missBySession.values()) if (latest === void 0 || record.at >= latest.at) latest = record;
10796
+ return latest;
10797
+ }
10798
+ return missBySession.get(sessionScopeKey(sessionId));
10799
+ }
10800
+ /**
10801
+ * Cache totals for one account's share of a session, one whole session, or every
10802
+ * tracked session.
10803
+ *
10804
+ * A missing session is not an error: the settings card has no conversation in
10805
+ * hand, and the process aggregate is the honest answer there.
10806
+ */
10807
+ function getCacheStats$1(sessionId, accountId) {
10808
+ const totals = sumTotals(sessionId, accountId);
10809
+ const prompt = totals.cachedTokens + totals.freshTokens;
10810
+ const miss = missFor(sessionId);
10811
+ return {
10812
+ ...totals,
10813
+ hitRatio: prompt === 0 ? null : totals.cachedTokens / prompt,
10814
+ ...miss === void 0 ? {} : { lastMiss: miss }
10815
+ };
10816
+ }
10817
+ //#endregion
10447
10818
  //#region src/host/antigravity/account-pool.ts
10448
10819
  function poolPath() {
10449
10820
  return path.join(dshHomeDir(), "storages", "antigravity-pool.json");
10450
10821
  }
10822
+ /**
10823
+ * The rotation strategy this line defaults to, and the reader for it.
10824
+ *
10825
+ * Sticky rather than the shared core's sequential default, because the account a
10826
+ * request is served by may also decide which server-side prefix cache it can
10827
+ * reach: each account is a separate quota domain, and an account that changes mid
10828
+ * conversation plausibly lands on a cache the previous one had not warmed, so the
10829
+ * next turn may reprocess a prefix that was already paid for. Holding the account
10830
+ * that served the previous request is the cheap way to not find out the hard way,
10831
+ * and a request that is rate limited still falls through to another account.
10832
+ *
10833
+ * That account identity selects a cache namespace is INFERRED, not measured.
10834
+ * Nothing here observes an account-scoped cache metric, no live capture has been
10835
+ * taken, and docs/antigravity-claude-cache-audit.md records the upstream position
10836
+ * — on-wire session affinity still needs transport diagnostics — against a run
10837
+ * that had a single account, so cross-account behaviour was never exercised.
10838
+ * The default stands as a design assumption until one of those is done.
10839
+ *
10840
+ * Only the DEFAULT differs. An explicit stored choice is returned unchanged, and
10841
+ * a pool file written before this change carries an explicit 'sequential' — so
10842
+ * upgrading does not silently move an existing user onto a different strategy.
10843
+ * The shared core keeps its own sequential default for every other line.
10844
+ */
10845
+ function poolRotationStrategy(value) {
10846
+ return value === "sequential" || value === "round-robin" || value === "sticky" ? value : "sticky";
10847
+ }
10451
10848
  function parseAntigravityPoolData(value) {
10452
10849
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Antigravity pool payload is invalid");
10453
10850
  const record = value;
10454
- const strategy = normalizeRotationStrategy(record.rotationStrategy);
10851
+ const strategy = poolRotationStrategy(record.rotationStrategy);
10455
10852
  const activeAccountId = typeof record.activeAccountId === "string" ? record.activeAccountId : void 0;
10456
10853
  const rawAccounts = Array.isArray(record.accounts) ? record.accounts : [];
10457
10854
  const accounts = [];
@@ -10680,6 +11077,25 @@ function resolveDefaultReasoningEffort$4(efforts, configuredEffort) {
10680
11077
  if (efforts.includes("low")) return ReasoningEffortId("low");
10681
11078
  if (efforts.length > 0) return ReasoningEffortId(efforts[0]);
10682
11079
  }
11080
+ /**
11081
+ * Land one stream chunk's usage on its session's cache totals.
11082
+ *
11083
+ * The adapter is the only place that still knows the request's session and the
11084
+ * account that served it by the time the terminal chunk arrives: usage is
11085
+ * emitted from the mapper's stream state, which carries neither. Chunks of every
11086
+ * other type pass through untouched, and the mapper is left alone.
11087
+ *
11088
+ * The mapper counts cached and fresh prompt tokens separately
11089
+ * (`cacheReadTokens` / `inputTokens`), which is exactly the split these totals
11090
+ * are built from, so the numbers here are the ones the harness already reported.
11091
+ */
11092
+ function recordUsageChunk(chunk, sessionId, accountId) {
11093
+ if (chunk.type !== "usage") return;
11094
+ recordCacheUsage({
11095
+ cachedTokens: chunk.usage.cacheReadTokens ?? 0,
11096
+ freshTokens: chunk.usage.inputTokens ?? 0
11097
+ }, sessionId, accountId);
11098
+ }
10683
11099
  var AntigravityAdapter = class extends LlmAdapter {
10684
11100
  store;
10685
11101
  modelSettings;
@@ -10793,6 +11209,8 @@ var AntigravityAdapter = class extends LlmAdapter {
10793
11209
  let recoveredMissingThinkingSignature = false;
10794
11210
  const triedAccountIds = /* @__PURE__ */ new Set();
10795
11211
  let response;
11212
+ let statsSessionId = options.sessionId === void 0 ? void 0 : String(options.sessionId);
11213
+ let statsAccountId;
10796
11214
  while (true) {
10797
11215
  let eff;
10798
11216
  try {
@@ -10813,6 +11231,9 @@ var AntigravityAdapter = class extends LlmAdapter {
10813
11231
  triedAccountIds.add(account.id);
10814
11232
  for (const runtimeModel of candidates) {
10815
11233
  const body = JSON.stringify(buildRequest$2(requestOptions, model, projectId, runtimeModel, effort, images));
11234
+ recordCacheRequest(body, requestOptions.sessionId);
11235
+ statsSessionId = requestOptions.sessionId === void 0 ? void 0 : String(requestOptions.sessionId);
11236
+ statsAccountId = account.id;
10816
11237
  const headers = {
10817
11238
  ...antigravityHeaders(token),
10818
11239
  ...model.id.startsWith("claude-") ? { "anthropic-beta": "interleaved-thinking-2025-05-14" } : {}
@@ -10903,16 +11324,25 @@ var AntigravityAdapter = class extends LlmAdapter {
10903
11324
  const trimmed = line.trim();
10904
11325
  if (!trimmed) continue;
10905
11326
  const chunks = processStreamLine$2(trimmed, state);
10906
- for (const chunk of chunks) yield chunk;
11327
+ for (const chunk of chunks) {
11328
+ recordUsageChunk(chunk, statsSessionId, statsAccountId);
11329
+ yield chunk;
11330
+ }
10907
11331
  if (state.finished) return;
10908
11332
  }
10909
11333
  }
10910
11334
  buffer += decoder.decode();
10911
11335
  if (buffer.trim()) {
10912
11336
  const chunks = processStreamLine$2(buffer.trim(), state);
10913
- for (const chunk of chunks) yield chunk;
11337
+ for (const chunk of chunks) {
11338
+ recordUsageChunk(chunk, statsSessionId, statsAccountId);
11339
+ yield chunk;
11340
+ }
11341
+ }
11342
+ for (const chunk of closeStream$5(state)) {
11343
+ recordUsageChunk(chunk, statsSessionId, statsAccountId);
11344
+ yield chunk;
10914
11345
  }
10915
- for (const chunk of closeStream$5(state)) yield chunk;
10916
11346
  } finally {
10917
11347
  reader.cancel().catch(() => void 0);
10918
11348
  }
@@ -11004,7 +11434,7 @@ async function readRequestJson$7(request) {
11004
11434
  request.on("error", reject);
11005
11435
  });
11006
11436
  }
11007
- async function getAntigravityWebStatus(store, modelSettings, preferences, accountPool = new AccountPoolStore(void 0, void 0, store)) {
11437
+ async function getAntigravityWebStatus(store, modelSettings, preferences, accountPool = new AccountPoolStore(void 0, void 0, store), sessionId) {
11008
11438
  const credentials = await store.read();
11009
11439
  const settings = preferences ? preferences.status() : await modelSettings.read();
11010
11440
  const quota = getCachedQuota();
@@ -11035,16 +11465,32 @@ async function getAntigravityWebStatus(store, modelSettings, preferences, accoun
11035
11465
  defaultReasoningEffort: settings.defaultReasoningEffort || null,
11036
11466
  accounts,
11037
11467
  activeAccountId: poolData?.activeAccountId || activeAccount?.id,
11038
- rotationStrategy: poolData?.rotationStrategy || "sequential"
11468
+ rotationStrategy: poolData?.rotationStrategy || poolRotationStrategy(void 0),
11469
+ cache: cacheStatsOrNull$1(sessionId)
11039
11470
  };
11040
11471
  }
11472
+ /**
11473
+ * Rolling cache totals for one session, or null while nothing has reported usage.
11474
+ *
11475
+ * The session is a query parameter because the numbers are per session: a host
11476
+ * running several conversations at once cannot present one merged hit ratio as
11477
+ * if it described any of them. With no session named the route still answers
11478
+ * with the process aggregate, which is the right answer for a settings card
11479
+ * that is not showing a specific conversation.
11480
+ */
11481
+ function cacheStatsOrNull$1(sessionId) {
11482
+ const stats = getCacheStats$1(sessionId);
11483
+ if (stats.requests === 0) return null;
11484
+ return stats;
11485
+ }
11041
11486
  function registerAntigravityRoutes(ctx, store, modelSettings, preferences, fetchFn = fetch, accountPool = new AccountPoolStore(void 0, void 0, store)) {
11042
11487
  const quotaRefresh = new QuotaRefresh();
11043
11488
  return ctx.webServer.register({
11044
11489
  kind: "prefix",
11045
11490
  path: "/antigravity/api",
11046
11491
  handler: async (request, response) => {
11047
- const path = new URL(request.url || "/", "http://dsh.local").pathname.replace(/^\/antigravity\/api\/?/, "");
11492
+ const url = new URL(request.url || "/", "http://dsh.local");
11493
+ const path = url.pathname.replace(/^\/antigravity\/api\/?/, "");
11048
11494
  try {
11049
11495
  if (path === "status" || path === "") {
11050
11496
  if (request.method !== "GET") return sendMethodNotAllowed$6(response);
@@ -11059,7 +11505,7 @@ function registerAntigravityRoutes(ctx, store, modelSettings, preferences, fetch
11059
11505
  return sendJson$8(response, 200, {
11060
11506
  ok: true,
11061
11507
  value: {
11062
- ...await getAntigravityWebStatus(store, modelSettings, preferences, accountPool),
11508
+ ...await getAntigravityWebStatus(store, modelSettings, preferences, accountPool, url.searchParams.get("sessionId") ?? void 0),
11063
11509
  quotaRefreshing: quotaRefresh.refreshing
11064
11510
  }
11065
11511
  });
@@ -30217,6 +30663,25 @@ function refreshSourceForDomain(domain) {
30217
30663
  }
30218
30664
  //#endregion
30219
30665
  //#region src/shared/workbuddy-contracts.ts
30666
+ /**
30667
+ * Parse one gateway \`credits\` string into the bare multiplier it names.
30668
+ *
30669
+ * Measured shapes: \`x6.67\` (intl), \`x0.79 credits\` (cn), and \`''\` for a model
30670
+ * whose publisher declares no rate. \`x0.00\` is deliberately kept — it is a real
30671
+ * declaration ("this model does not consume allowance"), not a missing value,
30672
+ * so it must not be confused with the empty string that means nothing is known.
30673
+ *
30674
+ * The return is \`null\` rather than \`0\` for anything unparseable, so a caller can
30675
+ * tell "no rate published" from "a rate of zero".
30676
+ */
30677
+ function parseWorkBuddyCreditMultiplier(raw) {
30678
+ if (typeof raw !== "string") return null;
30679
+ const stripped = raw.replace(/\s*credits\s*$/i, "").trim().replace(/^[x×]/i, "").trim();
30680
+ if (stripped === "") return null;
30681
+ const value = Number(stripped);
30682
+ if (!Number.isFinite(value) || value < 0) return null;
30683
+ return stripped;
30684
+ }
30220
30685
  /** Every level this route can name, in escalating order (the card's sort key). */
30221
30686
  const WORKBUDDY_REASONING_EFFORTS = [
30222
30687
  "minimal",
@@ -30390,6 +30855,7 @@ const FALLBACK_MODELS$1 = [
30390
30855
  maxContextWindow: 56e3,
30391
30856
  maxTokens: 24e3,
30392
30857
  regions: ["cn"],
30858
+ credits: { cn: "2.00" },
30393
30859
  supportsImage: false,
30394
30860
  reasoningEfforts: [],
30395
30861
  defaultReasoningEffort: null,
@@ -30403,6 +30869,7 @@ const FALLBACK_MODELS$1 = [
30403
30869
  maxContextWindow: 2e5,
30404
30870
  maxTokens: 32e3,
30405
30871
  regions: ["intl"],
30872
+ credits: { intl: "0.34" },
30406
30873
  supportsImage: true,
30407
30874
  reasoningEfforts: [
30408
30875
  "low",
@@ -30420,6 +30887,7 @@ const FALLBACK_MODELS$1 = [
30420
30887
  maxContextWindow: 256e3,
30421
30888
  maxTokens: 32e3,
30422
30889
  regions: ["intl"],
30890
+ credits: { intl: "0.59" },
30423
30891
  supportsImage: true,
30424
30892
  reasoningEfforts: [
30425
30893
  "low",
@@ -30437,6 +30905,7 @@ const FALLBACK_MODELS$1 = [
30437
30905
  maxContextWindow: 272e3,
30438
30906
  maxTokens: 72e3,
30439
30907
  regions: ["intl"],
30908
+ credits: { intl: "3.31" },
30440
30909
  supportsImage: true,
30441
30910
  reasoningEfforts: [
30442
30911
  "low",
@@ -30454,6 +30923,7 @@ const FALLBACK_MODELS$1 = [
30454
30923
  maxContextWindow: 2e5,
30455
30924
  maxTokens: 24e3,
30456
30925
  regions: ["intl"],
30926
+ credits: { intl: "3.33" },
30457
30927
  supportsImage: true,
30458
30928
  reasoningEfforts: [],
30459
30929
  defaultReasoningEffort: null,
@@ -30468,6 +30938,7 @@ const FALLBACK_MODELS$1 = [
30468
30938
  maxContextWindow: 1e6,
30469
30939
  maxTokens: 128e3,
30470
30940
  regions: ["intl"],
30941
+ credits: { intl: "6.67" },
30471
30942
  supportsImage: true,
30472
30943
  reasoningEfforts: [
30473
30944
  "low",
@@ -30487,6 +30958,7 @@ const FALLBACK_MODELS$1 = [
30487
30958
  maxContextWindow: 1e6,
30488
30959
  maxTokens: 128e3,
30489
30960
  regions: ["intl"],
30961
+ credits: { intl: "3.47" },
30490
30962
  supportsImage: true,
30491
30963
  reasoningEfforts: [
30492
30964
  "low",
@@ -30506,6 +30978,7 @@ const FALLBACK_MODELS$1 = [
30506
30978
  maxContextWindow: 1e6,
30507
30979
  maxTokens: 128e3,
30508
30980
  regions: ["intl"],
30981
+ credits: { intl: "1.39" },
30509
30982
  supportsImage: true,
30510
30983
  reasoningEfforts: [
30511
30984
  "low",
@@ -30525,6 +30998,7 @@ const FALLBACK_MODELS$1 = [
30525
30998
  maxContextWindow: 1e6,
30526
30999
  maxTokens: 128e3,
30527
31000
  regions: ["intl"],
31001
+ credits: { intl: "0.14" },
30528
31002
  supportsImage: true,
30529
31003
  reasoningEfforts: [
30530
31004
  "low",
@@ -30544,6 +31018,7 @@ const FALLBACK_MODELS$1 = [
30544
31018
  maxContextWindow: 1e6,
30545
31019
  maxTokens: 128e3,
30546
31020
  regions: ["intl"],
31021
+ credits: { intl: "3.31" },
30547
31022
  supportsImage: true,
30548
31023
  reasoningEfforts: [
30549
31024
  "low",
@@ -30562,6 +31037,7 @@ const FALLBACK_MODELS$1 = [
30562
31037
  maxContextWindow: 272e3,
30563
31038
  maxTokens: 72e3,
30564
31039
  regions: ["intl"],
31040
+ credits: { intl: "1.65" },
30565
31041
  supportsImage: true,
30566
31042
  reasoningEfforts: [
30567
31043
  "low",
@@ -30580,6 +31056,10 @@ const FALLBACK_MODELS$1 = [
30580
31056
  maxContextWindow: 1e6,
30581
31057
  maxTokens: 48e3,
30582
31058
  regions: ["intl", "cn"],
31059
+ credits: {
31060
+ cn: "0.79",
31061
+ intl: "0.79"
31062
+ },
30583
31063
  supportsImage: true,
30584
31064
  reasoningEfforts: [
30585
31065
  "low",
@@ -30597,6 +31077,10 @@ const FALLBACK_MODELS$1 = [
30597
31077
  maxContextWindow: 1e6,
30598
31078
  maxTokens: 32e3,
30599
31079
  regions: ["intl", "cn"],
31080
+ credits: {
31081
+ cn: "0.06",
31082
+ intl: "0.06"
31083
+ },
30600
31084
  supportsImage: true,
30601
31085
  reasoningEfforts: [
30602
31086
  "low",
@@ -30614,6 +31098,10 @@ const FALLBACK_MODELS$1 = [
30614
31098
  maxContextWindow: 1e6,
30615
31099
  maxTokens: 48e3,
30616
31100
  regions: ["intl", "cn"],
31101
+ credits: {
31102
+ cn: "0.79",
31103
+ intl: "0.79"
31104
+ },
30617
31105
  supportsImage: true,
30618
31106
  reasoningEfforts: ["high", "xhigh"],
30619
31107
  defaultReasoningEffort: "high",
@@ -30627,6 +31115,7 @@ const FALLBACK_MODELS$1 = [
30627
31115
  maxContextWindow: 2e5,
30628
31116
  maxTokens: 48e3,
30629
31117
  regions: ["cn"],
31118
+ credits: { cn: "0.79" },
30630
31119
  supportsImage: true,
30631
31120
  reasoningEfforts: [
30632
31121
  "low",
@@ -30644,6 +31133,7 @@ const FALLBACK_MODELS$1 = [
30644
31133
  maxContextWindow: 2e5,
30645
31134
  maxTokens: 64e3,
30646
31135
  regions: ["cn"],
31136
+ credits: { cn: "0.71" },
30647
31137
  supportsImage: true,
30648
31138
  reasoningEfforts: [
30649
31139
  "low",
@@ -30661,6 +31151,7 @@ const FALLBACK_MODELS$1 = [
30661
31151
  maxContextWindow: 2e5,
30662
31152
  maxTokens: 48e3,
30663
31153
  regions: ["cn"],
31154
+ credits: { cn: "0.80" },
30664
31155
  supportsImage: false,
30665
31156
  reasoningEfforts: [],
30666
31157
  defaultReasoningEffort: null,
@@ -30674,6 +31165,7 @@ const FALLBACK_MODELS$1 = [
30674
31165
  maxContextWindow: 2e5,
30675
31166
  maxTokens: 48e3,
30676
31167
  regions: ["cn"],
31168
+ credits: { cn: "0.95" },
30677
31169
  supportsImage: false,
30678
31170
  reasoningEfforts: [
30679
31171
  "low",
@@ -30691,6 +31183,7 @@ const FALLBACK_MODELS$1 = [
30691
31183
  maxContextWindow: 2e5,
30692
31184
  maxTokens: 48e3,
30693
31185
  regions: ["cn"],
31186
+ credits: { cn: "0.23" },
30694
31187
  supportsImage: false,
30695
31188
  reasoningEfforts: [],
30696
31189
  defaultReasoningEffort: null,
@@ -30704,6 +31197,7 @@ const FALLBACK_MODELS$1 = [
30704
31197
  maxContextWindow: 128e3,
30705
31198
  maxTokens: 32e3,
30706
31199
  regions: ["cn"],
31200
+ credits: { cn: "0.11" },
30707
31201
  supportsImage: true,
30708
31202
  reasoningEfforts: [
30709
31203
  "low",
@@ -30721,6 +31215,7 @@ const FALLBACK_MODELS$1 = [
30721
31215
  maxContextWindow: 168e3,
30722
31216
  maxTokens: 32e3,
30723
31217
  regions: ["cn"],
31218
+ credits: { cn: "0.23" },
30724
31219
  supportsImage: false,
30725
31220
  reasoningEfforts: [],
30726
31221
  defaultReasoningEffort: null,
@@ -30734,6 +31229,7 @@ const FALLBACK_MODELS$1 = [
30734
31229
  maxContextWindow: 1e6,
30735
31230
  maxTokens: 32e3,
30736
31231
  regions: ["intl"],
31232
+ credits: { intl: "1.62" },
30737
31233
  supportsImage: true,
30738
31234
  reasoningEfforts: [
30739
31235
  "low",
@@ -30751,6 +31247,7 @@ const FALLBACK_MODELS$1 = [
30751
31247
  maxContextWindow: 1e6,
30752
31248
  maxTokens: 32e3,
30753
31249
  regions: ["cn"],
31250
+ credits: { cn: "1.62" },
30754
31251
  supportsImage: true,
30755
31252
  reasoningEfforts: [
30756
31253
  "low",
@@ -30768,6 +31265,10 @@ const FALLBACK_MODELS$1 = [
30768
31265
  maxContextWindow: 1e6,
30769
31266
  maxTokens: 32e3,
30770
31267
  regions: ["intl", "cn"],
31268
+ credits: {
31269
+ cn: "0.77",
31270
+ intl: "0.77"
31271
+ },
30771
31272
  supportsImage: true,
30772
31273
  reasoningEfforts: [
30773
31274
  "low",
@@ -30785,6 +31286,7 @@ const FALLBACK_MODELS$1 = [
30785
31286
  maxContextWindow: 256e3,
30786
31287
  maxTokens: 32e3,
30787
31288
  regions: ["cn"],
31289
+ credits: { cn: "0.57" },
30788
31290
  supportsImage: true,
30789
31291
  reasoningEfforts: [
30790
31292
  "low",
@@ -30802,6 +31304,10 @@ const FALLBACK_MODELS$1 = [
30802
31304
  maxContextWindow: 256e3,
30803
31305
  maxTokens: 32e3,
30804
31306
  regions: ["intl", "cn"],
31307
+ credits: {
31308
+ cn: "0.52",
31309
+ intl: "0.52"
31310
+ },
30805
31311
  supportsImage: true,
30806
31312
  reasoningEfforts: [
30807
31313
  "low",
@@ -30819,6 +31325,7 @@ const FALLBACK_MODELS$1 = [
30819
31325
  maxContextWindow: 164e3,
30820
31326
  maxTokens: 32e3,
30821
31327
  regions: ["cn"],
31328
+ credits: { cn: "0.45" },
30822
31329
  supportsImage: true,
30823
31330
  reasoningEfforts: [
30824
31331
  "low",
@@ -30836,6 +31343,7 @@ const FALLBACK_MODELS$1 = [
30836
31343
  maxContextWindow: 164e3,
30837
31344
  maxTokens: 32e3,
30838
31345
  regions: ["cn"],
31346
+ credits: { cn: "0.54" },
30839
31347
  supportsImage: false,
30840
31348
  reasoningEfforts: [
30841
31349
  "low",
@@ -30853,6 +31361,10 @@ const FALLBACK_MODELS$1 = [
30853
31361
  maxContextWindow: 1e6,
30854
31362
  maxTokens: 128e3,
30855
31363
  regions: ["intl", "cn"],
31364
+ credits: {
31365
+ cn: "0.11",
31366
+ intl: "0.00"
31367
+ },
30856
31368
  supportsImage: true,
30857
31369
  reasoningEfforts: [
30858
31370
  "low",
@@ -30870,6 +31382,7 @@ const FALLBACK_MODELS$1 = [
30870
31382
  maxContextWindow: 1e6,
30871
31383
  maxTokens: 128e3,
30872
31384
  regions: ["intl"],
31385
+ credits: { intl: "0.03" },
30873
31386
  supportsImage: true,
30874
31387
  reasoningEfforts: [
30875
31388
  "low",
@@ -30887,6 +31400,7 @@ const FALLBACK_MODELS$1 = [
30887
31400
  maxContextWindow: 1e6,
30888
31401
  maxTokens: 128e3,
30889
31402
  regions: ["cn"],
31403
+ credits: { cn: "0.51" },
30890
31404
  supportsImage: true,
30891
31405
  reasoningEfforts: [
30892
31406
  "low",
@@ -30904,6 +31418,7 @@ const FALLBACK_MODELS$1 = [
30904
31418
  maxContextWindow: 1e6,
30905
31419
  maxTokens: 5e4,
30906
31420
  regions: ["cn"],
31421
+ credits: { cn: "0.17" },
30907
31422
  supportsImage: true,
30908
31423
  reasoningEfforts: [
30909
31424
  "low",
@@ -30921,6 +31436,7 @@ const FALLBACK_MODELS$1 = [
30921
31436
  maxContextWindow: 96e3,
30922
31437
  maxTokens: 32e3,
30923
31438
  regions: ["cn"],
31439
+ credits: { cn: "0.29" },
30924
31440
  supportsImage: false,
30925
31441
  reasoningEfforts: [
30926
31442
  "low",
@@ -30938,6 +31454,10 @@ const FALLBACK_MODELS$1 = [
30938
31454
  maxContextWindow: 1e6,
30939
31455
  maxTokens: 64e3,
30940
31456
  regions: ["intl", "cn"],
31457
+ credits: {
31458
+ cn: "0.29",
31459
+ intl: "0.29"
31460
+ },
30941
31461
  supportsImage: true,
30942
31462
  reasoningEfforts: ["high"],
30943
31463
  defaultReasoningEffort: "high",
@@ -30951,6 +31471,7 @@ const FALLBACK_MODELS$1 = [
30951
31471
  maxContextWindow: 1e6,
30952
31472
  maxTokens: 64e3,
30953
31473
  regions: ["cn"],
31474
+ credits: { cn: "0.00" },
30954
31475
  supportsImage: true,
30955
31476
  reasoningEfforts: ["high"],
30956
31477
  defaultReasoningEffort: "high",
@@ -30964,6 +31485,10 @@ const FALLBACK_MODELS$1 = [
30964
31485
  maxContextWindow: 192e3,
30965
31486
  maxTokens: 64e3,
30966
31487
  regions: ["intl", "cn"],
31488
+ credits: {
31489
+ cn: "0.00",
31490
+ intl: "0.00"
31491
+ },
30967
31492
  supportsImage: true,
30968
31493
  reasoningEfforts: ["low", "high"],
30969
31494
  defaultReasoningEffort: "high",
@@ -30977,6 +31502,7 @@ const FALLBACK_MODELS$1 = [
30977
31502
  maxContextWindow: 192e3,
30978
31503
  maxTokens: 64e3,
30979
31504
  regions: ["cn"],
31505
+ credits: { cn: "0.05" },
30980
31506
  supportsImage: true,
30981
31507
  reasoningEfforts: [
30982
31508
  "low",
@@ -30994,6 +31520,7 @@ const FALLBACK_MODELS$1 = [
30994
31520
  maxContextWindow: 2e5,
30995
31521
  maxTokens: 8192,
30996
31522
  regions: ["cn"],
31523
+ credits: { cn: "0.10" },
30997
31524
  supportsImage: false,
30998
31525
  reasoningEfforts: [],
30999
31526
  defaultReasoningEffort: null,
@@ -31007,6 +31534,7 @@ const FALLBACK_MODELS$1 = [
31007
31534
  maxContextWindow: 1e6,
31008
31535
  maxTokens: 128e3,
31009
31536
  regions: ["cn"],
31537
+ credits: { cn: "0.08" },
31010
31538
  supportsImage: true,
31011
31539
  reasoningEfforts: [
31012
31540
  "low",
@@ -31026,6 +31554,7 @@ const FALLBACK_MODELS$1 = [
31026
31554
  maxContextWindow: 512e3,
31027
31555
  maxTokens: 64e3,
31028
31556
  regions: ["cn"],
31557
+ credits: { cn: "0.25" },
31029
31558
  supportsImage: true,
31030
31559
  reasoningEfforts: [
31031
31560
  "low",
@@ -31043,6 +31572,7 @@ const FALLBACK_MODELS$1 = [
31043
31572
  maxContextWindow: 2e5,
31044
31573
  maxTokens: 48e3,
31045
31574
  regions: ["cn"],
31575
+ credits: { cn: "0.19" },
31046
31576
  supportsImage: true,
31047
31577
  reasoningEfforts: [
31048
31578
  "low",
@@ -31060,6 +31590,7 @@ const FALLBACK_MODELS$1 = [
31060
31590
  maxContextWindow: 2e5,
31061
31591
  maxTokens: 48e3,
31062
31592
  regions: ["cn"],
31593
+ credits: { cn: "0.18" },
31063
31594
  supportsImage: false,
31064
31595
  reasoningEfforts: [
31065
31596
  "low",
@@ -31078,6 +31609,7 @@ const FALLBACK_MODELS$1 = [
31078
31609
  maxContextWindow: 1e6,
31079
31610
  maxTokens: 65536,
31080
31611
  regions: ["intl"],
31612
+ credits: { intl: "0.99" },
31081
31613
  supportsImage: true,
31082
31614
  reasoningEfforts: [
31083
31615
  "low",
@@ -31145,6 +31677,7 @@ function resolveWorkBuddyModel(id, catalog = FALLBACK_MODELS$1) {
31145
31677
  maxContextWindow: DEFAULT_CONTEXT_WINDOW$1,
31146
31678
  maxTokens: FALLBACK_MAX_TOKENS,
31147
31679
  regions: ["cn", "intl"],
31680
+ credits: {},
31148
31681
  supportsImage: false,
31149
31682
  reasoningEfforts: [],
31150
31683
  defaultReasoningEffort: null,
@@ -32151,6 +32684,14 @@ async function fetchAccountIdentity$1(credentials, options = {}) {
32151
32684
  * inferred from the model's name. Image-generation tools (`tags` naming
32152
32685
  * `text-to-image`/`image-to-image`) are skipped: they are not chat models and
32153
32686
  * carry no context window.
32687
+ *
32688
+ * `credits` is read here rather than left to the shipped table because a
32689
+ * reachable gateway **replaces** that table entirely: a multiplier transcribed
32690
+ * offline would be the only one a signed-in user ever saw, and it would stay
32691
+ * wrong the moment the subscription repriced a model. A string this route cannot
32692
+ * turn into a number (`''`, which `default-model` publishes, or a value a
32693
+ * future gateway invents) leaves the field off the entry altogether instead of
32694
+ * writing a plausible-looking `0` — see `parseWorkBuddyCreditMultiplier`.
32154
32695
  */
32155
32696
  function parseConfigModels(payload, region) {
32156
32697
  const root = asRecord$4(payload) ?? {};
@@ -32172,6 +32713,7 @@ function parseConfigModels(payload, region) {
32172
32713
  const declaredDefault = asString$5(reasoning.defaultEffort) ?? single ?? null;
32173
32714
  const modelDefault = declaredDefault === null || efforts.includes(declaredDefault) ? declaredDefault : convergeWorkBuddyEffort(declaredDefault, efforts);
32174
32715
  const servedDefault = asNumber$1(asRecord$4(record.contextWindow)?.defaultLength);
32716
+ const multiplier = parseWorkBuddyCreditMultiplier(record.credits);
32175
32717
  models.push({
32176
32718
  id,
32177
32719
  name: asString$5(record.name) ?? id,
@@ -32179,6 +32721,7 @@ function parseConfigModels(payload, region) {
32179
32721
  maxContextWindow,
32180
32722
  maxTokens: asNumber$1(record.maxOutputTokens) ?? 32768,
32181
32723
  regions: [region],
32724
+ ...multiplier === null ? {} : { credits: { [region]: multiplier } },
32182
32725
  supportsImage: record.supportsImages === true,
32183
32726
  reasoningEfforts: efforts,
32184
32727
  defaultReasoningEffort: modelDefault,
@@ -32792,6 +33335,13 @@ function resolveEnabledModelIds$2(stored, available, enabled = true) {
32792
33335
  * Order comes from the caller, which passes a vendor-grouped list so a family
32793
33336
  * is not interleaved with the next one; this preserves whatever order it is
32794
33337
  * given rather than imposing a second one.
33338
+ *
33339
+ * `credits` is passed through whole, every region the entry carries. Trimming
33340
+ * it to one account's region is tempting and wrong: this DTO is also read with
33341
+ * no account in hand (the harness resolves a model list before a credential is
33342
+ * chosen), and such a caller would be told nothing rather than everything. The
33343
+ * region key the reader actually has is already on each entry as `regions`, so
33344
+ * picking a value is the renderer's job, not this one's.
32795
33345
  */
32796
33346
  function buildModelOptions(catalog, available, enabledModelIds, overrides) {
32797
33347
  const enabled = new Set(enabledModelIds);
@@ -32809,6 +33359,7 @@ function buildModelOptions(catalog, available, enabledModelIds, overrides) {
32809
33359
  reasoningEfforts: [...workBuddyReasoningEfforts(id, catalog)],
32810
33360
  supportsImage: workBuddyModelSupportsImage(id, catalog),
32811
33361
  regions: [...entry.regions],
33362
+ ...entry.credits === void 0 || Object.keys(entry.credits).length === 0 ? {} : { credits: { ...entry.credits } },
32812
33363
  description: entry.description
32813
33364
  };
32814
33365
  });