@mars-sea/dsh-commandcode-provider 0.11.0 → 0.11.2

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/lib/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { GenerateOptions, LlmAdapter, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ResolvedRetryPolicy, StreamChunk } from "@deepseek-ai/dsh-llm";
2
+ import { GenerateOptions, LlmAdapter, LlmImageRequestPricing, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, Message, ResolvedRetryPolicy, StreamChunk } from "@deepseek-ai/dsh-llm";
3
3
  import { CredentialRef } from "@deepseek-ai/dsh-credentials";
4
4
  import { TypertRemoteService, TypertSchema } from "@deepseek-ai/dsh-typert-protocol";
5
5
  import { WebRuntime, WebSearchProvider, WebSearchRequest, WebSearchResult } from "@deepseek-ai/dsh-web";
6
6
  import { Context } from "@deepseek-ai/cordis";
7
- import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
7
+ import { AttachmentStore, ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
8
8
  import { CommandDefinition } from "@deepseek-ai/dsh-commands";
9
9
  //#region src/adapter.d.ts
10
10
  declare const COMMAND_CODE_CLI_VERSION = "1.54.0";
@@ -22,6 +22,27 @@ declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000;
22
22
  declare function projectSlugFromPath(pathName: string): string;
23
23
  /** Read a usable Command Code credential from the official CLI auth file. */
24
24
  declare function resolveAuthFileApiKey(): string | undefined;
25
+ /** One occurrence's exact request-version byte length, as the core counter wants it. */
26
+ type ImageVersionBytes = (block: {
27
+ attachment: ImageAttachmentRef;
28
+ }) => number;
29
+ /**
30
+ * The installed engine's request-image policy, whichever generation it is.
31
+ *
32
+ * Every member is optional because the two generations never coexist: a
33
+ * ≤0.1.5 engine has only `offloadRequestImagesWithPolicy`, a ≥0.1.6 engine has
34
+ * only the other two. `CORE_IMAGE_POLICY` is the namespace object viewed
35
+ * through this shape, and `surfaceImagePolicy()` is the ONE place that decides
36
+ * which contract this process speaks.
37
+ */
38
+ interface CoreImagePolicy {
39
+ /** ≤0.1.5: project history under a byte/count budget, adapter-owned and transient. */
40
+ offloadRequestImagesWithPolicy?: (messages: readonly Message[], policy: Record<string, unknown>) => readonly Message[];
41
+ /** ≥0.1.6: render the surface's durable offload marks as placeholder text. */
42
+ projectOffloadedImages?: (messages: readonly Message[], placeholder: (ref: ImageAttachmentRef) => string) => readonly Message[];
43
+ /** ≥0.1.6: how many more leading retained occurrences must be offloaded. */
44
+ requiredImageOffload?: (messages: readonly Message[], budget: Record<string, unknown>, versionBytes: ImageVersionBytes) => number;
45
+ }
25
46
  /** Connection facts resolved fresh per request by the plugin entry. */
26
47
  interface CommandCodeConnectionOptions {
27
48
  /** API base; the Provider API lives under it (`/alpha/generate`, `/provider/v1/chat/completions`, `/provider/v1/models`). */
@@ -102,6 +123,13 @@ interface CommandCodeAdapterDeps<C extends CommandCodeConnectionOptions = Comman
102
123
  fetchImpl?: typeof fetch;
103
124
  /** Resolve the optional durable attachment service for image input (tests); defaults to none. */
104
125
  resolveAttachments?: ResolveAttachments;
126
+ /**
127
+ * Request-image policy override (tests); defaults to the installed engine's
128
+ * own helpers, whichever generation they belong to. Injecting it is the only
129
+ * way to exercise the ≥0.1.6 durable-offload contract while this checkout
130
+ * still compiles against the 0.1.2 peers.
131
+ */
132
+ imageOffload?: CoreImagePolicy;
105
133
  }
106
134
  /** Account identity from `/alpha/whoami`. */
107
135
  interface CommandCodeAccount {
@@ -202,6 +230,15 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
202
230
  private catalog;
203
231
  private readonly fetchImpl;
204
232
  private readonly resolveAttachments;
233
+ /** The engine's request-image policy, resolved once (tests may inject one). */
234
+ private readonly imagePolicy;
235
+ /**
236
+ * The ≥0.1.6 durable contract, or `undefined` on a ≤0.1.5 engine — where the
237
+ * adapter must evict its own history. Resolved once: the contract a process
238
+ * speaks cannot change while it runs, and resolving here keeps every request
239
+ * on the same branch (see {@link surfaceImagePolicy}).
240
+ */
241
+ private readonly surfaceOffload;
205
242
  private readonly billingAccess;
206
243
  private readonly billingAccessInflight;
207
244
  private readonly protocolCache;
@@ -232,6 +269,22 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
232
269
  * future config knob for it would apply on profile restart, not per request.
233
270
  */
234
271
  providerRetryPolicy(_provider: string): ResolvedRetryPolicy;
272
+ /**
273
+ * Visual-token pricing for one exact model route (see `./image-tokens.ts`).
274
+ *
275
+ * Without this the token meter prices EVERY image with its structural
276
+ * heuristic — a handful of tokens for a full screenshot — so an image-heavy
277
+ * session reports far less context than it is actually carrying. The price is
278
+ * computed at the dimensions this route would send (the same request target
279
+ * `readImageRequest` encodes to), so the estimate tracks the wire rather than
280
+ * the stored original.
281
+ *
282
+ * Both payload generations are handled, because they disagree: ≤0.1.5 hands
283
+ * over bare `ImageAttachmentRef`s, ≥0.1.6 hands over `ImageBlock`s that also
284
+ * carry the surface's `offloaded` mark. Returning a price per occurrence, in
285
+ * order, is a hard requirement — the meter throws when the counts differ.
286
+ */
287
+ imageRequestPricing(_provider: string, model: string): LlmImageRequestPricing | undefined;
235
288
  /** Refresh the catalog (live fetch, cache fallback) and return it. */
236
289
  private loadCatalog;
237
290
  listModels(provider: string, opts?: {
@@ -1555,6 +1608,15 @@ interface Config {
1555
1608
  * Defaults to true.
1556
1609
  */
1557
1610
  webSearch?: boolean;
1611
+ /**
1612
+ * Whether the Web sidebar shows the plans & quota card
1613
+ * (`sidebar.footer.action`). Defaults to false: the card is opt-in, so an
1614
+ * unset document renders no quota surface in the sidebar and mounts no
1615
+ * background usage poll for it. Only the sidebar entry is affected — the
1616
+ * dashboard cell behind it stays registered, it simply has no trigger until
1617
+ * the toggle is on. Read by the browser client; the adapter ignores it.
1618
+ */
1619
+ showSidebarQuota?: boolean;
1558
1620
  /**
1559
1621
  * Language override for the `/commandcode` Host-side command's user-facing
1560
1622
  * copy. Host commands cannot read the client's `ctx.locale`, so this is
package/lib/index.js CHANGED
@@ -2,7 +2,8 @@ import { homedir } from "node:os";
2
2
  import { dirname, join } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
5
- import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, isContextWindowExceededError, offloadRequestImagesWithPolicy, offloadedImageText, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
+ import * as dshLlm from "@deepseek-ai/dsh-llm";
6
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, isContextWindowExceededError, offloadedImageText, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
6
7
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
7
8
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
8
9
  import { existsSync, readFileSync } from "node:fs";
@@ -2072,6 +2073,235 @@ function normalizeResolvedKey(value) {
2072
2073
  return trimmed === "" ? void 0 : trimmed;
2073
2074
  }
2074
2075
  //#endregion
2076
+ //#region src/image-request.ts
2077
+ /**
2078
+ * Long edge one request image may keep, in pixels.
2079
+ *
2080
+ * 1568 is Anthropic's documented ceiling — their API downscales anything larger
2081
+ * before the model sees it — and it sits inside the high-detail band of the
2082
+ * OpenAI and Gemini tile schemes, so no upstream this gateway reaches reads
2083
+ * more detail than the target keeps. Screenshots from a Retina display are the
2084
+ * case this is for: 2880x1800 becomes 1568x980, a 3.4x pixel cut, and the
2085
+ * models lose nothing they would not have downscaled themselves.
2086
+ */
2087
+ const REQUEST_IMAGE_MAX_LONG_EDGE = 1568;
2088
+ /**
2089
+ * Encoded bytes one request image may keep, before base64 expansion. The same
2090
+ * 1 MiB budget the harness's own multi-provider adapter defaults to; the
2091
+ * attachment service keeps its smallest quality-ladder output when no quality
2092
+ * fits, so this bounds bytes without ever refusing an image.
2093
+ */
2094
+ const REQUEST_IMAGE_MAX_ENCODED_BYTES = 1048576;
2095
+ /**
2096
+ * Aspect-preserving projection onto a long-edge budget; never enlarges, and
2097
+ * never returns a zero dimension (a 1-px degenerate side is what the round of
2098
+ * an extreme aspect ratio can produce, and a 0 would make the encoder refuse).
2099
+ *
2100
+ * A local restatement of the helper 0.1.6 exports from `dsh-attachment`, which
2101
+ * 0.1.2–0.1.5 do not have; the math is the whole contract, so keeping the copy
2102
+ * here costs less than a version probe would.
2103
+ */
2104
+ function longEdgeDimensions(width, height, longEdge) {
2105
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return {
2106
+ width: Math.max(1, Math.round(width) || 1),
2107
+ height: Math.max(1, Math.round(height) || 1)
2108
+ };
2109
+ if (longEdge >= Math.max(width, height)) return {
2110
+ width,
2111
+ height
2112
+ };
2113
+ return width >= height ? {
2114
+ width: longEdge,
2115
+ height: Math.max(1, Math.round(longEdge * height / width))
2116
+ } : {
2117
+ width: Math.max(1, Math.round(longEdge * width / height)),
2118
+ height: longEdge
2119
+ };
2120
+ }
2121
+ /**
2122
+ * The request target for one normalized attachment: its aspect ratio under the
2123
+ * long-edge budget, plus the byte budget every generation honours.
2124
+ *
2125
+ * The area is computed from the PROJECTED dimensions rather than the source's,
2126
+ * so ≤0.1.5's own pixel projection lands on the same geometry instead of
2127
+ * re-deriving a slightly different one from the original.
2128
+ */
2129
+ function requestImageTarget(ref) {
2130
+ const projected = longEdgeDimensions(ref.width, ref.height, REQUEST_IMAGE_MAX_LONG_EDGE);
2131
+ return {
2132
+ maxPixels: projected.width * projected.height,
2133
+ width: projected.width,
2134
+ height: projected.height,
2135
+ maxBytes: REQUEST_IMAGE_MAX_ENCODED_BYTES
2136
+ };
2137
+ }
2138
+ //#endregion
2139
+ //#region src/image-tokens.ts
2140
+ /**
2141
+ * Vision-token accounting for the model families Command Code routes to.
2142
+ *
2143
+ * The harness's token meter prices request images through
2144
+ * `LlmAdapter.imageRequestPricing(provider, model).priceImages(blocks)` and,
2145
+ * when a route declares nothing, falls back to a structural estimate that
2146
+ * charges a handful of tokens for ANY image. On an image-heavy session that is
2147
+ * not a rounding error: a screenshot loop can carry dozens of full-size images
2148
+ * that the heuristic prices at ~0 tokens each, so context pressure is
2149
+ * under-reported and the session walks into its own context window.
2150
+ *
2151
+ * This route is a gateway in front of many upstreams — one catalog id per model,
2152
+ * but the visual-token rule belongs to whoever serves it. So the estimate is per
2153
+ * FAMILY, using each vendor's published rule, and the families are recognised by
2154
+ * the catalog id (which names the vendor). Every formula is stated here rather
2155
+ * than imported: the only in-tree implementations live inside the official
2156
+ * adapters, and this plugin deliberately does not depend on those protocols.
2157
+ *
2158
+ * These are ESTIMATES for context accounting, not invoices — the provider's own
2159
+ * usage remains authoritative for a completed request, exactly as the published
2160
+ * per-token rates the session-cost readout uses are.
2161
+ *
2162
+ * @module dsh-commandcode-provider/image-tokens
2163
+ */
2164
+ /**
2165
+ * Which family prices one catalog model id.
2166
+ *
2167
+ * `generic` is not "we do not know the model" but "we cannot tell its visual
2168
+ * rule apart from the known ones" — the Qwen, MiniMax, xAI, moonshot and
2169
+ * sakana families all publish patch- or tile-based accounting close to the
2170
+ * Anthropic rule, so the conservative fallback below covers them without
2171
+ * pretending to more precision than we have.
2172
+ */
2173
+ function imageTokenFamily(modelId) {
2174
+ const id = modelId.toLowerCase();
2175
+ const vendor = id.includes("/") ? id.slice(0, id.indexOf("/")) : "";
2176
+ if (id.startsWith("claude")) return "anthropic";
2177
+ if (id.startsWith("gpt") || /^o[1-9]/.test(id)) return "openai";
2178
+ if (vendor === "google" || id.startsWith("gemini")) return "google";
2179
+ if (vendor === "deepseek") return "deepseek";
2180
+ return "generic";
2181
+ }
2182
+ /**
2183
+ * Visual tokens one request image costs on the given family, at the dimensions
2184
+ * this route would actually send (see `./image-request.ts`).
2185
+ */
2186
+ function imageTokenCost(family, width, height) {
2187
+ switch (family) {
2188
+ case "anthropic": return anthropicImageTokens(width, height);
2189
+ case "openai": return openAiImageTokens(width, height);
2190
+ case "google": return googleImageTokens(width, height);
2191
+ case "deepseek": return deepSeekImageTokens(width, height);
2192
+ default: return genericImageTokens(width, height);
2193
+ }
2194
+ }
2195
+ /** Convenience for callers that have a model id: family lookup plus the rule. */
2196
+ function commandCodeImageTokens(modelId, width, height) {
2197
+ return imageTokenCost(imageTokenFamily(modelId), width, height);
2198
+ }
2199
+ /**
2200
+ * Anthropic: the image is first scaled so its long edge is at most 1568 px
2201
+ * (never enlarged), then charged roughly one token per 750 pixels.
2202
+ * Source: Anthropic vision documentation, "Image size and token usage".
2203
+ */
2204
+ function anthropicImageTokens(width, height) {
2205
+ const projected = longEdgeDimensions(width, height, 1568);
2206
+ return Math.max(1, Math.ceil(projected.width * projected.height / 750));
2207
+ }
2208
+ /**
2209
+ * OpenAI (high detail): scale to fit inside 2048x2048, then scale the shortest
2210
+ * side to 768 px, then charge an 85-token base plus 170 tokens per 512x512
2211
+ * tile. Source: OpenAI vision documentation, "Calculating costs".
2212
+ */
2213
+ function openAiImageTokens(width, height) {
2214
+ let scaled = {
2215
+ width: Math.max(1, width),
2216
+ height: Math.max(1, height)
2217
+ };
2218
+ const longest = Math.max(scaled.width, scaled.height);
2219
+ if (longest > 2048) scaled = scaleBy(scaled, 2048 / longest);
2220
+ const shortest = Math.min(scaled.width, scaled.height);
2221
+ if (shortest > 768) scaled = scaleBy(scaled, 768 / shortest);
2222
+ return 85 + 170 * (Math.ceil(scaled.width / 512) * Math.ceil(scaled.height / 512));
2223
+ }
2224
+ /**
2225
+ * Google Gemini: an image whose both sides are at most 384 px costs a flat 258
2226
+ * tokens; anything larger is cropped into 768x768 tiles of 258 tokens each.
2227
+ * Source: Gemini image-understanding documentation.
2228
+ */
2229
+ function googleImageTokens(width, height) {
2230
+ if (width <= 384 && height <= 384) return 258;
2231
+ return 258 * Math.ceil(width / 768) * Math.ceil(height / 768);
2232
+ }
2233
+ /** 14-px patches, merged 3x3 into one token cell. */
2234
+ const DEEPSEEK_PATCH_SIZE = 14;
2235
+ const DEEPSEEK_DOWNSAMPLE_RATIO = 3;
2236
+ /** The provider's cap on tokens for one request image. */
2237
+ const DEEPSEEK_MAX_IMAGE_TOKENS = 1024;
2238
+ /** Total-pixel floor; a smaller image is enlarged before grid projection. */
2239
+ const DEEPSEEK_MIN_PIXELS = 295936;
2240
+ /**
2241
+ * DeepSeek: pad to 14-px patches, merge each 3x3 patch block into one token
2242
+ * cell, charge one token per cell plus one separator per row plus two framing
2243
+ * tokens, and cap one image at 1024 tokens by downscaling it aspect-preserving
2244
+ * (images below 544x544 are enlarged first).
2245
+ *
2246
+ * Mirrors `deepSeekImageTokens` in the official `dsh-llm-deepseek` adapter
2247
+ * (its `PATCH_SIZE`, `DOWNSAMPLE_RATIO`, `MAX_IMAGE_TOKENS` and `MIN_PIXELS`).
2248
+ * The official solver picks the largest grid that fits the cap; this restates
2249
+ * that as a bounded search over the scale factor, which lands on the same grid
2250
+ * without vendoring the closed-form solver.
2251
+ */
2252
+ function deepSeekImageTokens(width, height) {
2253
+ let scaled = {
2254
+ width: Math.max(1, width),
2255
+ height: Math.max(1, height)
2256
+ };
2257
+ const pixels = scaled.width * scaled.height;
2258
+ if (pixels < DEEPSEEK_MIN_PIXELS) scaled = scaleBy(scaled, Math.sqrt(DEEPSEEK_MIN_PIXELS / pixels));
2259
+ const direct = deepSeekGridTokens(scaled.width, scaled.height);
2260
+ if (direct <= DEEPSEEK_MAX_IMAGE_TOKENS) return direct;
2261
+ let low = 0;
2262
+ let high = 1;
2263
+ let best = direct;
2264
+ for (let step = 0; step < 24; step += 1) {
2265
+ const mid = (low + high) / 2;
2266
+ const tokens = deepSeekGridTokens(Math.max(1, Math.round(scaled.width * mid)), Math.max(1, Math.round(scaled.height * mid)));
2267
+ if (tokens <= DEEPSEEK_MAX_IMAGE_TOKENS) {
2268
+ low = mid;
2269
+ best = tokens;
2270
+ } else high = mid;
2271
+ }
2272
+ return best;
2273
+ }
2274
+ /** One token grid's charge: every row carries a separator, plus two framing tokens. */
2275
+ function deepSeekGridTokens(width, height) {
2276
+ const cellsWide = cellCount(width);
2277
+ return cellCount(height) * (cellsWide + 1) + 2;
2278
+ }
2279
+ /** Token cells along one padded pixel axis. */
2280
+ function cellCount(length) {
2281
+ const patches = Math.ceil(length / DEEPSEEK_PATCH_SIZE);
2282
+ return Math.ceil(patches / DEEPSEEK_DOWNSAMPLE_RATIO);
2283
+ }
2284
+ /**
2285
+ * The conservative fallback for a family we cannot price exactly: the MOST
2286
+ * expensive of the three simple published rules at these dimensions.
2287
+ *
2288
+ * Chosen deliberately. Under-reporting context is what lets a session walk into
2289
+ * its own context window, while over-reporting only makes the meter cautious;
2290
+ * for the real families this covers (Qwen's 28-px patches, MiniMax, xAI,
2291
+ * moonshot, sakana) the Anthropic rule is the close one, and it is the maximum
2292
+ * at every size except the sub-384-px band, where Gemini's flat 258 wins.
2293
+ */
2294
+ function genericImageTokens(width, height) {
2295
+ return Math.max(anthropicImageTokens(width, height), openAiImageTokens(width, height), googleImageTokens(width, height));
2296
+ }
2297
+ /** Scale one dimension pair by a positive factor, rounded to whole pixels. */
2298
+ function scaleBy(size, factor) {
2299
+ return {
2300
+ width: Math.max(1, Math.round(size.width * factor)),
2301
+ height: Math.max(1, Math.round(size.height * factor))
2302
+ };
2303
+ }
2304
+ //#endregion
2075
2305
  //#region src/adapter.ts
2076
2306
  /**
2077
2307
  * DeepSeek Harness LLM adapter for the Command Code Provider API.
@@ -2556,17 +2786,19 @@ const REQUEST_IMAGE_BUDGETS = [{
2556
2786
  countQuantum: 12
2557
2787
  }];
2558
2788
  /**
2559
- * Project request history under one image budget: the oldest images past it
2560
- * become placeholder text in place, the newest keep their bytes, and a history
2561
- * already inside the budget is returned as the SAME array — the caller uses
2562
- * that identity to tell "nothing evicted" from "evicted".
2789
+ * Project request history under one image budget (≤0.1.5 engines only): the
2790
+ * oldest images past it become placeholder text in place, the newest keep their
2791
+ * bytes, and a history already inside the budget is returned as the SAME array
2792
+ * — the caller uses that identity to tell "nothing evicted" from "evicted".
2563
2793
  *
2564
2794
  * Nested tool-result images ride the same core walk, which is exactly right
2565
2795
  * for this adapter: an evicted `read_image` result surfaces its placeholder as
2566
2796
  * the tool message's own text, so no carrier user message is emitted for it.
2567
2797
  */
2568
- function projectRequestImages(messages, budget) {
2569
- return offloadRequestImagesWithPolicy(messages, {
2798
+ function projectRequestImages(messages, budget, policy) {
2799
+ const offload = policy.offloadRequestImagesWithPolicy;
2800
+ if (typeof offload !== "function") return messages;
2801
+ return offload(messages, {
2570
2802
  representation: "base64",
2571
2803
  maxBytes: budget.maxBytes,
2572
2804
  maxImages: budget.maxImages,
@@ -2582,14 +2814,128 @@ function projectRequestImages(messages, budget) {
2582
2814
  * the raw history — otherwise a retry could reintroduce images the first rung
2583
2815
  * had already evicted.
2584
2816
  */
2585
- function withImageBudget(options, budget) {
2586
- const projected = projectRequestImages(options.messages, budget);
2817
+ function withImageBudget(options, budget, policy) {
2818
+ const projected = projectRequestImages(options.messages, budget, policy);
2587
2819
  return projected === options.messages ? options : {
2588
2820
  ...options,
2589
2821
  messages: [...projected]
2590
2822
  };
2591
2823
  }
2592
2824
  /**
2825
+ * Stable provider-neutral code for "a request still carries more images than
2826
+ * this route will send; offload N of them and retry". Declared locally rather
2827
+ * than imported: the constant does not exist on the 0.1.2–0.1.5 peers this
2828
+ * plugin still supports, and importing it statically is the link-time failure
2829
+ * described above the budget constants.
2830
+ */
2831
+ const IMAGE_OFFLOAD_REQUIRED_CODE = "IMAGE_OFFLOAD_REQUIRED";
2832
+ /** The installed engine's image policy; see {@link CoreImagePolicy}. */
2833
+ const CORE_IMAGE_POLICY = dshLlm;
2834
+ /**
2835
+ * The durable-offload contract this engine speaks, or undefined on ≤0.1.5.
2836
+ *
2837
+ * Both members are required together: a half-populated engine would either
2838
+ * lose the surface's marks (re-sending evicted images as pixels) or lose the
2839
+ * count (silently sending an over-budget body), and neither is worth a partial
2840
+ * opt-in. Absent, the adapter keeps its own transient projection instead.
2841
+ */
2842
+ function surfaceImagePolicy(api = CORE_IMAGE_POLICY) {
2843
+ return typeof api.projectOffloadedImages === "function" && typeof api.requiredImageOffload === "function" ? {
2844
+ projectOffloadedImages: api.projectOffloadedImages,
2845
+ requiredImageOffload: api.requiredImageOffload
2846
+ } : void 0;
2847
+ }
2848
+ /**
2849
+ * The failure a route raises instead of evicting on its own (≥0.1.6). The
2850
+ * count is the ONE thing the harness needs: `dsh-compaction-image-offload`
2851
+ * records it as an `image/offload` event, marks that many oldest retained
2852
+ * occurrences, and retries the step.
2853
+ *
2854
+ * Deliberately outside `providerRetryPolicy()`'s whitelist: a byte-identical
2855
+ * resend cannot succeed, so the retry belongs to the surface mutation, not to
2856
+ * dsh-llm-retry. On a profile that does not mount that plugin the failure is
2857
+ * terminal — every shipped profile reaches it through dsh-base.
2858
+ */
2859
+ function imageOffloadRequired(missing) {
2860
+ const options = { offloadImages: missing };
2861
+ return new LlmError(`commandcode request images exceed the route budget; ${missing} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, options);
2862
+ }
2863
+ /** The budget in the shape the core counter takes (representation + the rung). */
2864
+ function coreBudget(budget) {
2865
+ return {
2866
+ representation: "base64",
2867
+ ...budget
2868
+ };
2869
+ }
2870
+ /**
2871
+ * Exact request-version bytes of one retained occurrence. The declared
2872
+ * normalized size IS what this adapter inlines (the body is built from
2873
+ * `attachments.readImage`, base64-encoded), so the accounting matches the wire
2874
+ * byte for byte, and the core helper expands it for `base64` on its own.
2875
+ */
2876
+ const imageVersionBytes = (block) => block.attachment.bytes;
2877
+ /**
2878
+ * ≥0.1.6 request options: the surface's durable marks become placeholder text,
2879
+ * and an over-budget history FAILS with the count to offload instead of being
2880
+ * evicted here. Throws rather than returning, so no caller can accidentally
2881
+ * send a body this route already knows is too large.
2882
+ */
2883
+ function withSurfaceOffload(options, policy, budget) {
2884
+ const marked = policy.projectOffloadedImages(options.messages, (ref) => offloadedImageText(ref));
2885
+ const messages = marked === options.messages ? options.messages : [...marked];
2886
+ const missing = policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes);
2887
+ if (missing > 0) throw imageOffloadRequired(missing);
2888
+ return messages === options.messages ? options : {
2889
+ ...options,
2890
+ messages
2891
+ };
2892
+ }
2893
+ /**
2894
+ * One image's bytes for the wire: the attachment service's REQUEST VERSION when
2895
+ * it can produce one, else the normalized original.
2896
+ *
2897
+ * `readImage()` answers the stored original, which is what admission accepted —
2898
+ * not what a provider request should carry. `readImageRequest()` re-encodes to
2899
+ * the route target in `./image-request.ts` and caches the result under it, so
2900
+ * the same attachment costs one encode per target instead of one per occurrence,
2901
+ * and the base64 the wire expands is bounded (both the pixel budget and, more to
2902
+ * the point here, the encoded bytes). The fallback is for a backend that does
2903
+ * not implement the abstract method; every engine this plugin declares support
2904
+ * for does.
2905
+ */
2906
+ async function readRequestImage(attachments, ref) {
2907
+ if (typeof attachments.readImageRequest === "function") return (await attachments.readImageRequest(ref, requestImageTarget(ref))).data;
2908
+ return (await attachments.readImage(ref)).data;
2909
+ }
2910
+ /**
2911
+ * Price one request occurrence.
2912
+ *
2913
+ * Two cases cost no vision tokens at all, and both are priced as their
2914
+ * model-visible TEXT so the caller's estimator can charge that instead: a model
2915
+ * that accepts text only (the harness projects those images to placeholder text
2916
+ * before the request), and an occurrence the session surface has already
2917
+ * offloaded (≥0.1.6 renders the same placeholder text in every later request).
2918
+ * A retained occurrence on this route contributes no text at all — the pixels
2919
+ * are inlined — so its price is the family's visual-token count and nothing else.
2920
+ */
2921
+ function priceRequestImage(occurrence, model, imageCapable) {
2922
+ const block = occurrence;
2923
+ const ref = block.attachment ?? occurrence;
2924
+ if (!imageCapable) return {
2925
+ visualTokens: 0,
2926
+ text: textOnlyImageText(ref)
2927
+ };
2928
+ if (block.offloaded === true) return {
2929
+ visualTokens: 0,
2930
+ text: offloadedImageText(ref)
2931
+ };
2932
+ const target = requestImageTarget(ref);
2933
+ return {
2934
+ visualTokens: commandCodeImageTokens(model, target.width, target.height),
2935
+ text: ""
2936
+ };
2937
+ }
2938
+ /**
2593
2939
  * Convert one image reference to the Command Code wire format, as the official
2594
2940
  * CLI does: `{ type: 'image', source: { type: 'base64', media_type, data } }`.
2595
2941
  * Bytes come from the durable attachment service; the media type is the one
@@ -3106,6 +3452,15 @@ var CommandCodeAdapter = class extends LlmAdapter {
3106
3452
  catalog = [];
3107
3453
  fetchImpl;
3108
3454
  resolveAttachments;
3455
+ /** The engine's request-image policy, resolved once (tests may inject one). */
3456
+ imagePolicy;
3457
+ /**
3458
+ * The ≥0.1.6 durable contract, or `undefined` on a ≤0.1.5 engine — where the
3459
+ * adapter must evict its own history. Resolved once: the contract a process
3460
+ * speaks cannot change while it runs, and resolving here keeps every request
3461
+ * on the same branch (see {@link surfaceImagePolicy}).
3462
+ */
3463
+ surfaceOffload;
3109
3464
  billingAccess = /* @__PURE__ */ new Map();
3110
3465
  billingAccessInflight = /* @__PURE__ */ new Map();
3111
3466
  protocolCache = /* @__PURE__ */ new Map();
@@ -3114,6 +3469,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
3114
3469
  this.deps = deps;
3115
3470
  this.fetchImpl = deps.fetchImpl ?? fetch;
3116
3471
  this.resolveAttachments = deps.resolveAttachments;
3472
+ this.imagePolicy = deps.imageOffload ?? CORE_IMAGE_POLICY;
3473
+ this.surfaceOffload = surfaceImagePolicy(this.imagePolicy);
3117
3474
  }
3118
3475
  /**
3119
3476
  * Display metadata for the picker's provider group header. The base class
@@ -3163,6 +3520,25 @@ var CommandCodeAdapter = class extends LlmAdapter {
3163
3520
  }
3164
3521
  }, "llm-commandcode: retryPolicy");
3165
3522
  }
3523
+ /**
3524
+ * Visual-token pricing for one exact model route (see `./image-tokens.ts`).
3525
+ *
3526
+ * Without this the token meter prices EVERY image with its structural
3527
+ * heuristic — a handful of tokens for a full screenshot — so an image-heavy
3528
+ * session reports far less context than it is actually carrying. The price is
3529
+ * computed at the dimensions this route would send (the same request target
3530
+ * `readImageRequest` encodes to), so the estimate tracks the wire rather than
3531
+ * the stored original.
3532
+ *
3533
+ * Both payload generations are handled, because they disagree: ≤0.1.5 hands
3534
+ * over bare `ImageAttachmentRef`s, ≥0.1.6 hands over `ImageBlock`s that also
3535
+ * carry the surface's `offloaded` mark. Returning a price per occurrence, in
3536
+ * order, is a hard requirement — the meter throws when the counts differ.
3537
+ */
3538
+ imageRequestPricing(_provider, model) {
3539
+ const imageCapable = KNOWN_IMAGE_MODELS.has(model);
3540
+ return { priceImages: (images) => images.map((image) => priceRequestImage(image, model, imageCapable)) };
3541
+ }
3166
3542
  /** Refresh the catalog (live fetch, cache fallback) and return it. */
3167
3543
  async loadCatalog(signal) {
3168
3544
  const { apiBase, modelsCachePath } = this.deps.options();
@@ -3449,7 +3825,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
3449
3825
  if (!KNOWN_IMAGE_MODELS.has(options.model)) throw new LlmError(`Command Code model "${options.model}" does not support image input; switch to a Vision-capable model (see the model registry)`, "UNSUPPORTED_CONTENT");
3450
3826
  const attachments = this.resolveAttachments?.();
3451
3827
  if (attachments === void 0) throw new LlmError("Command Code image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
3452
- readImage = (ref) => attachments.readImage(ref).then((stored) => stored.data);
3828
+ readImage = (ref) => readRequestImage(attachments, ref);
3453
3829
  }
3454
3830
  const connection = this.deps.options();
3455
3831
  let apiKey = await this.deps.resolveApiKey(connection, options.model);
@@ -3460,7 +3836,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
3460
3836
  const reasoningEffort = effort && effort !== "off" && supported?.includes(effort) ? effort : void 0;
3461
3837
  const systemText = [options.system ?? "", ...options.messages.filter((m) => m.role === "system").map((m) => m.content.map(blockText).filter(Boolean).join("\n"))].filter(Boolean).join("\n\n");
3462
3838
  let protocol = this.resolveProtocol(apiKey);
3463
- let requestOptions = withImageBudget(options, REQUEST_IMAGE_BUDGETS[0]);
3839
+ let requestOptions = this.surfaceOffload === void 0 ? withImageBudget(options, REQUEST_IMAGE_BUDGETS[0], this.imagePolicy) : withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0]);
3464
3840
  let imageBudgetIndex = 0;
3465
3841
  const buildBody = async (target) => target === "cli" ? buildCliBody(requestOptions, connection, {
3466
3842
  maxTokens,
@@ -3495,12 +3871,17 @@ var CommandCodeAdapter = class extends LlmAdapter {
3495
3871
  continue;
3496
3872
  }
3497
3873
  if (attempt.status === 413 && imageBudgetIndex + 1 < REQUEST_IMAGE_BUDGETS.length) {
3498
- const tightened = withImageBudget(requestOptions, REQUEST_IMAGE_BUDGETS[imageBudgetIndex + 1]);
3499
- if (tightened !== requestOptions) {
3500
- imageBudgetIndex += 1;
3501
- requestOptions = tightened;
3502
- body = await buildBody(protocol);
3503
- continue;
3874
+ if (this.surfaceOffload !== void 0) {
3875
+ const missing = this.surfaceOffload.requiredImageOffload(requestOptions.messages, coreBudget(REQUEST_IMAGE_BUDGETS[imageBudgetIndex + 1]), imageVersionBytes);
3876
+ if (missing > 0) throw imageOffloadRequired(missing);
3877
+ } else {
3878
+ const tightened = withImageBudget(requestOptions, REQUEST_IMAGE_BUDGETS[imageBudgetIndex + 1], this.imagePolicy);
3879
+ if (tightened !== requestOptions) {
3880
+ imageBudgetIndex += 1;
3881
+ requestOptions = tightened;
3882
+ body = await buildBody(protocol);
3883
+ continue;
3884
+ }
3504
3885
  }
3505
3886
  }
3506
3887
  const rotate = this.deps.rotateApiKey;
@@ -3972,8 +4353,7 @@ const commandcodeCommand = {
3972
4353
  resetSuffix: "重置 {when}",
3973
4354
  partialFailures: "⚠️ 部分端点失败: {list}",
3974
4355
  noData: "(无数据 — 请检查 API 密钥)",
3975
- errorText: "获取 Command Code 用量失败:{message}",
3976
- imageGate: "当前会话已包含图片,而模型 {model} 不支持图片输入;请选择支持图片的模型,或先移除会话中的图片。"
4356
+ errorText: "获取 Command Code 用量失败:{message}"
3977
4357
  },
3978
4358
  en: {
3979
4359
  title: "📊 Command Code usage{account}",
@@ -4004,8 +4384,7 @@ const commandcodeCommand = {
4004
4384
  resetSuffix: "resets {when}",
4005
4385
  partialFailures: "⚠️ some endpoints failed: {list}",
4006
4386
  noData: "(no data — check your API key)",
4007
- errorText: "Could not fetch Command Code usage: {message}",
4008
- imageGate: "This session already contains images, and model {model} does not accept image input; please select an image-capable model, or remove the images from the session first."
4387
+ errorText: "Could not fetch Command Code usage: {message}"
4009
4388
  }
4010
4389
  };
4011
4390
  /**
@@ -5638,6 +6017,7 @@ const Config = z.object({
5638
6017
  */
5639
6018
  modelVisibility: z.dict(z.boolean()),
5640
6019
  webSearch: z.boolean().default(true),
6020
+ showSidebarQuota: z.boolean().default(false),
5641
6021
  accounts: z.array(z.object({
5642
6022
  label: z.string(),
5643
6023
  apiKeyEnv: z.string().role("credential-ref"),