@mars-sea/dsh-commandcode-provider 0.11.1 → 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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/lib/client.js +140 -179
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +55 -2
- package/lib/index.js +400 -21
- package/lib/index.js.map +1 -1
- package/package.json +42 -38
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?: {
|
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
|
|
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
|
|
2560
|
-
* become placeholder text in place, the newest keep their
|
|
2561
|
-
* already inside the budget is returned as the SAME array
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
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
|
/**
|