@eddyskywalker/dsh-chatgpt-subscription 0.16.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.
- package/CHANGELOG.md +22 -0
- package/lib/client.js +121 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +465 -19
- package/lib/types/client/antigravity/AntigravitySection.d.ts.map +1 -1
- package/lib/types/client/antigravity/locales.d.ts +47 -2
- package/lib/types/client/antigravity/locales.d.ts.map +1 -1
- package/lib/types/client/kimi-code/locales.d.ts +2 -2
- package/lib/types/host/antigravity/account-pool.d.ts +24 -0
- package/lib/types/host/antigravity/account-pool.d.ts.map +1 -1
- package/lib/types/host/antigravity/adapter.d.ts.map +1 -1
- package/lib/types/host/antigravity/cache-stats.d.ts +166 -0
- package/lib/types/host/antigravity/cache-stats.d.ts.map +1 -0
- package/lib/types/host/antigravity/mapper.d.ts +75 -3
- package/lib/types/host/antigravity/mapper.d.ts.map +1 -1
- package/lib/types/host/antigravity/routes.d.ts +1 -1
- package/lib/types/host/antigravity/routes.d.ts.map +1 -1
- package/lib/types/shared/antigravity-contracts.d.ts +76 -0
- package/lib/types/shared/antigravity-contracts.d.ts.map +1 -1
- 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
|
-
/**
|
|
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)
|
|
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
|
-
/**
|
|
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 =
|
|
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)
|
|
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)
|
|
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 ||
|
|
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
|
|
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
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AntigravitySection.d.ts","sourceRoot":"","sources":["../../../../src/client/antigravity/AntigravitySection.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"AntigravitySection.d.ts","sourceRoot":"","sources":["../../../../src/client/antigravity/AntigravitySection.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAA;AAiBvE,UAAU,KAAK;IACb,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAC1B,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAA;CAChC;AAwBD,wBAAgB,kBAAkB,CAAC,EAAE,aAAa,EAAE,kBAAkB,EAAE,EAAE,KAAK,GAAG,KAAK,CAAC,YAAY,CAyiBnG"}
|