@arcfold/dsh 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/cordis.patch.yml +138 -0
  5. package/package.json +70 -4
  6. package/patches/deepseek-0.2.2-rc.1-transport.json +65 -0
  7. package/patches/deepseek-0.2.2-rc.1-transport.patch +61 -0
  8. package/presets/arcfold.patch.yml +110 -0
  9. package/runtime/README.md +19 -0
  10. package/runtime/bootstrap.json +15 -0
  11. package/runtime/desktop-python.tar.gz +0 -0
  12. package/runtime/host/package.json +11 -0
  13. package/runtime/host/pnpm-workspace.yaml +9 -0
  14. package/runtime/initial-policy.json +94 -0
  15. package/runtime/profile/pnpm-workspace.yaml +7 -0
  16. package/scripts/exact-package-patch.mjs +106 -0
  17. package/scripts/patch-deepseek-transport.mjs +14 -0
  18. package/src/agent.mjs +56 -0
  19. package/src/browse-scope.mjs +14 -0
  20. package/src/budget.mjs +369 -0
  21. package/src/business.mjs +334 -0
  22. package/src/context-material.mjs +119 -0
  23. package/src/deepseek-budget-plugin.mjs +58 -0
  24. package/src/deepseek-provider.mjs +57 -0
  25. package/src/deepseek-receipt.mjs +97 -0
  26. package/src/deepseek-transport.mjs +97 -0
  27. package/src/desktop/adapter.mjs +16 -0
  28. package/src/desktop/bootstrap.mjs +84 -0
  29. package/src/desktop/configuration.mjs +117 -0
  30. package/src/desktop/provider.mjs +46 -0
  31. package/src/desktop/python-runtime.mjs +105 -0
  32. package/src/desktop/setup-service.mjs +81 -0
  33. package/src/desktop/vendor/LICENSE.deepseek +21 -0
  34. package/src/desktop/vendor/NOTICE.md +5 -0
  35. package/src/desktop/vendor/deepseek-0.2.2-rc.1.mjs +2919 -0
  36. package/src/evidence-ownership.mjs +51 -0
  37. package/src/evidence.mjs +35 -0
  38. package/src/goal-context.mjs +52 -0
  39. package/src/goal-execution-tool.mjs +74 -0
  40. package/src/goal-execution.mjs +333 -0
  41. package/src/goal-tool.mjs +56 -0
  42. package/src/goal-work-control.mjs +63 -0
  43. package/src/goal-work.mjs +257 -0
  44. package/src/goals.mjs +578 -0
  45. package/src/importer.mjs +298 -0
  46. package/src/index.mjs +53 -0
  47. package/src/learning-contracts.mjs +220 -0
  48. package/src/learning-defaults.mjs +98 -0
  49. package/src/learning-goals.mjs +165 -0
  50. package/src/learning-plugin.mjs +28 -0
  51. package/src/learning-runner.mjs +275 -0
  52. package/src/learning.mjs +534 -0
  53. package/src/legacy-export.mjs +332 -0
  54. package/src/legacy-import-source.mjs +356 -0
  55. package/src/metered-provider-host.mjs +101 -0
  56. package/src/metered-transport.mjs +435 -0
  57. package/src/method-context.mjs +57 -0
  58. package/src/method-lifecycle.mjs +34 -0
  59. package/src/method-tool.mjs +61 -0
  60. package/src/methods.mjs +425 -0
  61. package/src/model-selection.mjs +41 -0
  62. package/src/native-context-call.mjs +88 -0
  63. package/src/native-continuation.mjs +217 -0
  64. package/src/native-request-authority.mjs +90 -0
  65. package/src/openai-models.mjs +4 -0
  66. package/src/openai-receipt.mjs +98 -0
  67. package/src/openai-transport.mjs +167 -0
  68. package/src/openai-usage.mjs +194 -0
  69. package/src/opportunities.mjs +295 -0
  70. package/src/preparation-artifacts.mjs +213 -0
  71. package/src/preparation-plugin.mjs +290 -0
  72. package/src/runtime-command.mjs +264 -0
  73. package/src/runtime-config.mjs +417 -0
  74. package/src/runtime-guard.mjs +284 -0
  75. package/src/runtime-host.mjs +212 -0
  76. package/src/runtime-init.mjs +278 -0
  77. package/src/runtime-installation.mjs +518 -0
  78. package/src/runtime-lifecycle.mjs +227 -0
  79. package/src/runtime-model-settings.mjs +111 -0
  80. package/src/runtime-readiness.mjs +285 -0
  81. package/src/runtime-workspace.mjs +18 -0
  82. package/src/session-bridge.mjs +365 -0
  83. package/src/store.mjs +394 -0
  84. package/src/tree/LICENSE +21 -0
  85. package/src/tree/attribution.mjs +120 -0
  86. package/src/tree/clusters.mjs +61 -0
  87. package/src/tree/consolidation.mjs +103 -0
  88. package/src/tree/context.mjs +211 -0
  89. package/src/tree/contracts.mjs +142 -0
  90. package/src/tree/experience.mjs +140 -0
  91. package/src/tree/experiment-budget.mjs +84 -0
  92. package/src/tree/hooks.mjs +254 -0
  93. package/src/tree/host-context.mjs +107 -0
  94. package/src/tree/host-process.mjs +288 -0
  95. package/src/tree/host.mjs +37 -0
  96. package/src/tree/improvement.mjs +106 -0
  97. package/src/tree/items.mjs +294 -0
  98. package/src/tree/jobs.mjs +184 -0
  99. package/src/tree/learning.mjs +202 -0
  100. package/src/tree/metrics.mjs +203 -0
  101. package/src/tree/model-owner.mjs +254 -0
  102. package/src/tree/observations.mjs +73 -0
  103. package/src/tree/prompts.mjs +500 -0
  104. package/src/tree/reports.mjs +67 -0
  105. package/src/tree/repository.mjs +236 -0
  106. package/src/tree/runtime.mjs +745 -0
  107. package/src/tree/script-writer.mjs +149 -0
  108. package/src/tree/scripts.mjs +208 -0
  109. package/src/tree/signals.mjs +118 -0
  110. package/src/tree/snapshots.mjs +379 -0
  111. package/src/tree/token-counter.mjs +75 -0
  112. package/src/tree/token-counter.py +37 -0
  113. package/src/tree/tokenizer-resources/LICENSE +21 -0
  114. package/src/tree/tokenizer-resources/README.md +31 -0
  115. package/src/tree/tokenizer-resources/deepseek_recipe-0.2.0-cp310-abi3-macosx_11_0_arm64.whl +0 -0
  116. package/src/tree/tokenizer-resources/manifest.json +61 -0
  117. package/src/tree/tokenizer-resources/native.pyi +1042 -0
  118. package/src/tree/tokenizer-resources/tokenizer-NOTICE.md +26 -0
  119. package/src/tree/tokenizer-resources/tokenizer-v4-NOTICE.md +27 -0
  120. package/src/tree/tokenizer-resources/tokenizer-v4.json +267359 -0
  121. package/src/tree/tokenizer-resources/tokenizer.json +267359 -0
  122. package/src/tree/tools.mjs +99 -0
  123. package/src/tree/tree.mjs +202 -0
  124. package/src/tree/ui.mjs +285 -0
  125. package/src/tree/verification.mjs +143 -0
  126. package/src/understandings.mjs +309 -0
  127. package/src/web-budget-recovery.mjs +181 -0
@@ -0,0 +1,2919 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { isVolatile } from "@deepseek-ai/cosmokit";
3
+ import { assertNever, deepEqualJson } from "@deepseek-ai/dsh-util-values";
4
+ import { APP_IDENTITY, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, RetryPolicySchema, ToolCallId, attributionHeaders, contentHasImage, errorChain, isContextWindowExceededError, isQuotaExceededError, offloadedImageText, projectOffloadedImages, requestImageHandleText, requiredImageOffload, resolveImageAttachmentAccess, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
5
+ import { MAX_TIMER_DELAY_MS, deadline, idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
6
+ import { ImageVariantId, longEdgeDimensions, requestImageDimensions } from "@deepseek-ai/dsh-attachment";
7
+ import { createHash } from "node:crypto";
8
+ import { mkdir, readFile } from "node:fs/promises";
9
+ import { dirname, join } from "node:path";
10
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
11
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
12
+ import { EventSourceParserStream } from "eventsource-parser/stream";
13
+ import { getOrCreateAnonymousUserId } from "@deepseek-ai/dsh-anonymous-user-id";
14
+ import semver from "semver";
15
+ //#region lib/types/models.js
16
+ /** Context window of both official models, as the official `/models` listing declares it. */
17
+ const OFFICIAL_CONTEXT_WINDOW = 1048576;
18
+ /** Advisory official model entries; deployments may replace the catalog. */
19
+ const DEFAULT_MODELS = [{
20
+ id: "deepseek-flash",
21
+ name: "DeepSeek-V4.1-Flash",
22
+ contextWindow: OFFICIAL_CONTEXT_WINDOW,
23
+ inputModalities: ["text", "image"],
24
+ systemPromptUpdate: "in-history",
25
+ toolUpdate: "addition-only"
26
+ }, {
27
+ id: "deepseek-v4-pro",
28
+ name: "DeepSeek-V4-Pro",
29
+ description: "Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.",
30
+ contextWindow: OFFICIAL_CONTEXT_WINDOW
31
+ }];
32
+ //#endregion
33
+ //#region lib/types/defaults.js
34
+ /** Shared provider limits and Chat Files API defaults. */
35
+ /** Default maximum idle interval while an adapter stream read is outstanding. */
36
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
37
+ /** Default combined request/response context capacity. */
38
+ const DEFAULT_CONTEXT_WINDOW = 1e6;
39
+ /** Default per-request output-token cap. */
40
+ const DEFAULT_MAX_TOKENS = 256e3;
41
+ /** Default bound on accumulated base64 image payload after Files API fallback. */
42
+ const DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
43
+ /** Deterministic raw-byte removal step. */
44
+ const DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM = 64 * 1024 * 1024;
45
+ /** Deterministic base64-byte removal step after Files API fallback. */
46
+ const DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM = 10 * 1024 * 1024;
47
+ /** Deterministic image-count removal step. */
48
+ const DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM = 20;
49
+ /** Default explicit lifetime for uploaded images. */
50
+ const DEFAULT_FILE_EXPIRY_SECONDS = 10080 * 60;
51
+ /** Default proactive refresh window for indexed file ids. */
52
+ const DEFAULT_FILE_REFRESH_MARGIN_SECONDS = 3600;
53
+ /** Default number of oldest harness-owned files removed on quota recovery. */
54
+ const DEFAULT_FILE_QUOTA_CLEANUP_BATCH = 100;
55
+ /** Default deadline for resolving one request image through the Files API. */
56
+ const DEFAULT_FILES_API_TIMEOUT_MS = 6e4;
57
+ //#endregion
58
+ //#region lib/types/image-tokens.js
59
+ /**
60
+ * DeepSeek vision-token accounting: the provider's published image-token
61
+ * calculator (api-docs.deepseek.com, Token & Token Usage) ported verbatim in
62
+ * its current `v41` configuration. The provider scales an image below
63
+ * 544×544 total pixels up, aligns it to a 14px-patch grid, downsamples 3:1
64
+ * per axis into token cells, and caps one image at 1024 tokens by solving the
65
+ * largest aspect-preserving grid inside that budget. The count is exact: this
66
+ * configuration has no alignment pad and no aspect-ratio clamp. Actual usage
67
+ * remains authoritative.
68
+ *
69
+ * @module dsh-llm-deepseek/image-tokens
70
+ */
71
+ /** Vision patch edge in pixels. */
72
+ const PATCH_SIZE = 14;
73
+ /** Per-axis patch-to-token downsampling ratio. */
74
+ const DOWNSAMPLE_RATIO = 3;
75
+ /** Provider cap on tokens for one request image. */
76
+ const MAX_IMAGE_TOKENS = 1024;
77
+ /** Total-pixel floor; smaller images are scaled up before grid projection. */
78
+ const MIN_PIXELS = 544 * 544;
79
+ /** Pixels covered by one token cell along either axis. */
80
+ const CELL_SIZE = PATCH_SIZE * DOWNSAMPLE_RATIO;
81
+ const intDiv = (value, divisor) => Math.floor(value / divisor);
82
+ const ceilDiv = (value, divisor) => Math.floor((value + divisor - 1) / divisor);
83
+ /** Token count of one grid: every row carries a separator, plus two framing tokens. */
84
+ function gridTokens(gridHeight, gridWidth) {
85
+ return gridHeight * (gridWidth + 1) + 2;
86
+ }
87
+ /** Token-cell count along one padded pixel axis. */
88
+ function gridCells(paddedLength) {
89
+ return ceilDiv(intDiv(paddedLength, PATCH_SIZE), DOWNSAMPLE_RATIO);
90
+ }
91
+ /** Solve the largest grid within `budget` tokens preserving the aspect ratio. */
92
+ function solveResizeRatio(height, width, budget) {
93
+ const aspect = height / width;
94
+ const idealGridWidth = Math.sqrt((budget - 2) / aspect + .25) - .5;
95
+ const idealGridHeight = idealGridWidth * aspect;
96
+ let bestHeight;
97
+ let bestWidth;
98
+ if (idealGridWidth < 1) {
99
+ const solvedGridWidth = 1;
100
+ const solvedGridHeight = intDiv(budget - 2, 2);
101
+ bestWidth = solvedGridWidth * CELL_SIZE;
102
+ bestHeight = solvedGridHeight * CELL_SIZE;
103
+ } else if (idealGridHeight < 1) {
104
+ const solvedGridHeight = 1;
105
+ bestWidth = (intDiv(budget - 2, solvedGridHeight) - 1) * CELL_SIZE;
106
+ bestHeight = solvedGridHeight * CELL_SIZE;
107
+ } else {
108
+ const solvedGridWidth = Math.trunc(idealGridWidth);
109
+ const solvedGridHeight = Math.trunc(idealGridHeight);
110
+ const scale = Math.min(solvedGridWidth * CELL_SIZE / width, solvedGridHeight * CELL_SIZE / height);
111
+ bestWidth = Math.trunc(width * scale / PATCH_SIZE) * PATCH_SIZE;
112
+ bestHeight = Math.trunc(height * scale / PATCH_SIZE) * PATCH_SIZE;
113
+ }
114
+ const gridHeight = gridCells(bestHeight);
115
+ const gridWidth = gridCells(bestWidth);
116
+ return {
117
+ gridHeight,
118
+ gridWidth,
119
+ bestHeight,
120
+ bestWidth,
121
+ numTokens: gridTokens(gridHeight, gridWidth)
122
+ };
123
+ }
124
+ /** Project padded pixel dimensions onto the largest in-budget token grid. */
125
+ function safeResize(height, width, paddedHeight, paddedWidth) {
126
+ const gridHeight = gridCells(paddedHeight);
127
+ const gridWidth = gridCells(paddedWidth);
128
+ const direct = {
129
+ gridHeight,
130
+ gridWidth,
131
+ bestHeight: paddedHeight,
132
+ bestWidth: paddedWidth,
133
+ numTokens: gridTokens(gridHeight, gridWidth)
134
+ };
135
+ if (direct.numTokens <= MAX_IMAGE_TOKENS) return direct;
136
+ const solved = solveResizeRatio(height, width, MAX_IMAGE_TOKENS);
137
+ /* v8 ignore next 3 -- the published solver's assertion; the closed-form
138
+ solve stays within the budget for every positive geometry. */
139
+ if (solved.numTokens > MAX_IMAGE_TOKENS) throw new Error(`deepseek image tokens: no grid fits the token budget for ${width}x${height}`);
140
+ return solved;
141
+ }
142
+ /** One scale-pad-project pass; the caller iterates it to a fixpoint. */
143
+ function resizeOnce(width, height) {
144
+ let scaledWidth = width;
145
+ let scaledHeight = height;
146
+ const pixels = scaledWidth * scaledHeight;
147
+ if (pixels < MIN_PIXELS && pixels > 0) {
148
+ const scale = Math.sqrt(MIN_PIXELS / pixels);
149
+ scaledWidth = Math.trunc(scaledWidth * scale);
150
+ scaledHeight = Math.trunc(scaledHeight * scale);
151
+ }
152
+ const paddedWidth = ceilDiv(scaledWidth, PATCH_SIZE) * PATCH_SIZE;
153
+ const paddedHeight = ceilDiv(scaledHeight, PATCH_SIZE) * PATCH_SIZE;
154
+ return safeResize(scaledHeight, scaledWidth, paddedHeight, paddedWidth);
155
+ }
156
+ function sameResize(a, b) {
157
+ return a.gridHeight === b.gridHeight && a.gridWidth === b.gridWidth && a.bestHeight === b.bestHeight && a.bestWidth === b.bestWidth && a.numTokens === b.numTokens;
158
+ }
159
+ /**
160
+ * Dimensions the harness sends so the provider keeps the whole image: the
161
+ * source itself when its patch-padded grid fits the token cap, otherwise the
162
+ * source aspect ratio at the solved grid's long edge. The provider pads the
163
+ * short edge to whole patches on its side. Rounding the aspect-preserving
164
+ * short edge can change the token count from the source's solved grid;
165
+ * request pricing uses the sent dimensions. Small images are never enlarged.
166
+ * @param width - positive integer source width in pixels.
167
+ * @param height - positive integer source height in pixels.
168
+ * @returns the request dimensions to encode.
169
+ */
170
+ function deepSeekRequestImageDimensions(width, height) {
171
+ const paddedWidth = ceilDiv(width, PATCH_SIZE) * PATCH_SIZE;
172
+ if (gridTokens(gridCells(ceilDiv(height, PATCH_SIZE) * PATCH_SIZE), gridCells(paddedWidth)) <= MAX_IMAGE_TOKENS) return {
173
+ width,
174
+ height
175
+ };
176
+ const solved = solveResizeRatio(height, width, MAX_IMAGE_TOKENS);
177
+ return longEdgeDimensions(width, height, width >= height ? solved.bestWidth : solved.bestHeight);
178
+ }
179
+ /**
180
+ * Vision tokens DeepSeek charges for one request image of the given
181
+ * dimensions.
182
+ * @param width - positive integer request-image width in pixels.
183
+ * @param height - positive integer request-image height in pixels.
184
+ * @returns the provider vision-token price, at most 1024.
185
+ */
186
+ function deepSeekImageTokens(width, height) {
187
+ let result = resizeOnce(width, height);
188
+ for (let iteration = 1; iteration < 10; iteration += 1) {
189
+ const next = resizeOnce(result.bestWidth, result.bestHeight);
190
+ if (sameResize(next, result)) return result.numTokens;
191
+ result = next;
192
+ }
193
+ /* v8 ignore next 2 -- the published solver's non-convergence guard; every
194
+ pass is a projection, so a second identical pass is a fixpoint. */
195
+ throw new Error(`deepseek image tokens: resize did not converge for ${width}x${height}`);
196
+ }
197
+ //#endregion
198
+ //#region lib/types/request-pricing.js
199
+ /**
200
+ * Provider-side request-image pricing for DeepSeek routes: prices every
201
+ * retained surface occurrence at its per-model request target with
202
+ * the published vision-token accounting, and every occurrence the surface
203
+ * marks offloaded as its placeholder text. Consumed synchronously by the
204
+ * token meter through `LlmAdapter.imageRequestPricing`; provider usage
205
+ * remains the authoritative anchor for completed requests.
206
+ *
207
+ * @module dsh-llm-deepseek/request-pricing
208
+ */
209
+ /** Default bound on accumulated file-referenced image bytes per request. */
210
+ const DEFAULT_MAX_REQUEST_FILES_BYTES = 128 * 1024 * 1024;
211
+ /** Provider request image-count limit. */
212
+ const DEFAULT_MAX_IMAGES_PER_REQUEST = 600;
213
+ /** Total-pixel budget matching provider low-detail image input. */
214
+ const DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET = 512 * 512;
215
+ /** Encoded-byte target for one deterministic model-request image; the smallest quality-ladder output is used when no quality fits. */
216
+ const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 2 * 1024 * 1024;
217
+ /**
218
+ * Provider per-side limit for a request carrying 15 or more images, applied
219
+ * to every request image so the image count never changes a projection.
220
+ */
221
+ const REQUEST_IMAGE_MAX_DIMENSION = 4096;
222
+ /**
223
+ * Resolve the encoded-byte target one DeepSeek model route applies to every request image.
224
+ * @param model - Advertised model route and its optional image overrides.
225
+ * @returns the route's encoded-byte target.
226
+ * @internal
227
+ */
228
+ function resolveRequestImageMaxBytes(model) {
229
+ return model.imageMaxBytes ?? 2097152;
230
+ }
231
+ /**
232
+ * Resolve the deterministic request target one DeepSeek model route chooses
233
+ * for one source image: the published token grid unless the model overrides
234
+ * it with a pixel budget, then the provider per-side limit, then the route's
235
+ * encoded-byte target. Small images are never enlarged.
236
+ * @param model - Advertised model route and its optional image overrides.
237
+ * @param source - intrinsic dimensions of the normalized attachment.
238
+ * @returns Complete request dimensions and encoded-byte target.
239
+ * @internal
240
+ */
241
+ function resolveRequestImageTarget(model, source) {
242
+ const budget = model.imagePixelBudget === "low" ? DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET : model.imagePixelBudget;
243
+ const projected = budget === void 0 ? deepSeekRequestImageDimensions(source.width, source.height) : requestImageDimensions(source.width, source.height, budget);
244
+ return {
245
+ ...Math.max(projected.width, projected.height) > 4096 ? longEdgeDimensions(source.width, source.height, REQUEST_IMAGE_MAX_DIMENSION) : projected,
246
+ maxBytes: resolveRequestImageMaxBytes(model)
247
+ };
248
+ }
249
+ /**
250
+ * Price one occurrence a text-only route substitutes with deterministic text,
251
+ * reproducing the `projectImagesForTextModel` substitution `LlmRuntime`
252
+ * applies before dispatching to a route without the `image` modality.
253
+ */
254
+ function textOnlyPrice(block) {
255
+ return {
256
+ visualTokens: 0,
257
+ text: textOnlyImageText(block.attachment)
258
+ };
259
+ }
260
+ /**
261
+ * Build the request-image pricing for one DeepSeek route from a validated
262
+ * connection snapshot. Uncatalogued and text-only models price every
263
+ * occurrence as its deterministic text substitution; image-capable models
264
+ * price an offloaded occurrence as its placeholder text and a retained one by
265
+ * its projected request dimensions, with each occurrence's handle or
266
+ * placeholder text built through the same access resolution the serializer
267
+ * uses. Access paths resolve at pricing time, so a path that changes before
268
+ * the request only shifts the text price by its own length.
269
+ * @param connection - validated connection facts of the pricing resolution.
270
+ * @param model - exact model id named by the request header.
271
+ * @param resolveAccess - current execution-world access resolution shared with request serialization.
272
+ * @returns synchronous per-occurrence pricing for the route.
273
+ */
274
+ function deepSeekImageRequestPricing(connection, model, resolveAccess) {
275
+ const catalogModel = connection.models.find((entry) => entry.id === model);
276
+ if (catalogModel?.inputModalities?.includes("image") !== true) return { priceImages: (images) => images.map(textOnlyPrice) };
277
+ return { priceImages: (images) => images.map(({ attachment: ref, offloaded }) => {
278
+ if (offloaded === true) return {
279
+ visualTokens: 0,
280
+ text: offloadedImageText(ref, resolveAccess?.(ref))
281
+ };
282
+ const target = resolveRequestImageTarget(catalogModel, ref);
283
+ return {
284
+ visualTokens: deepSeekImageTokens(target.width, target.height),
285
+ text: requestImageHandleText(ref, target, resolveAccess?.(ref))
286
+ };
287
+ }) };
288
+ }
289
+ //#endregion
290
+ //#region lib/types/config.js
291
+ const MODEL_MODALITIES = ["text", "image"];
292
+ /** Read the current value behind every reference of a validated Config.
293
+ * @param config Parsed plugin Config.
294
+ * @returns Plain options for the resolver.
295
+ */
296
+ function plainOptions(config) {
297
+ return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, isVolatile(value) ? value.get() : value]));
298
+ }
299
+ /**
300
+ * Schemas of the catalog-entry fields other than `id` and `inputModalities`,
301
+ * whose requiredness and defaults differ between a `models` entry and a
302
+ * provider's per-model overrides.
303
+ */
304
+ const catalogModelFields = {
305
+ name: z.string(),
306
+ description: z.string(),
307
+ contextWindow: z.number().step(1).min(1),
308
+ maxTokens: z.number().step(1).min(1),
309
+ imagePixelBudget: z.union([z.number().step(1).min(1), "low"]),
310
+ imageMaxBytes: z.number().step(1).min(1),
311
+ systemPromptUpdate: z.const("in-history"),
312
+ toolUpdate: z.union(["in-history", "addition-only"])
313
+ };
314
+ const catalogModel = z.object({
315
+ id: z.string().required(),
316
+ ...catalogModelFields,
317
+ inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(["text"])
318
+ });
319
+ /** Shared schema fields for Messages protocol options. */
320
+ const deepSeekConfigFields = {
321
+ baseURL: z.string().volatile(),
322
+ thinking: z.union(["enabled", "disabled"]).volatile(),
323
+ reasoningEffort: z.union([
324
+ "off",
325
+ "low",
326
+ "high",
327
+ "max"
328
+ ]).volatile(),
329
+ maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS).volatile(),
330
+ defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW).volatile(),
331
+ models: z.array(catalogModel).default(DEFAULT_MODELS).volatile(),
332
+ streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS).volatile(),
333
+ maxRequestFilesBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_FILES_BYTES).volatile(),
334
+ maxInlineRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES).volatile(),
335
+ maxImagesPerRequest: z.number().step(1).min(1).default(600).volatile(),
336
+ imageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM).volatile(),
337
+ inlineImageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM).volatile(),
338
+ imageOffloadCountQuantum: z.number().step(1).min(1).default(20).volatile(),
339
+ filesApiTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_FILES_API_TIMEOUT_MS).volatile(),
340
+ fileExpiresAfterSeconds: z.number().step(1).min(3600).max(2592e3).default(DEFAULT_FILE_EXPIRY_SECONDS).volatile(),
341
+ fileRefreshMarginSeconds: z.number().step(1).min(0).default(DEFAULT_FILE_REFRESH_MARGIN_SECONDS).volatile(),
342
+ fileQuotaCleanupBatch: z.number().step(1).min(1).max(1e3).default(100).volatile(),
343
+ retryPolicy: RetryPolicySchema.volatile()
344
+ };
345
+ const Config = z.object(deepSeekConfigFields);
346
+ /** Public API default; the internal endpoint comes from $DEEPSEEK_BASE_URL. */
347
+ const PUBLIC_BASE_URL = "https://api.deepseek.com/anthropic";
348
+ /** Environment variable naming this provider's endpoint, honored only from trusted layers. */
349
+ const BASE_URL_ENV = "DEEPSEEK_BASE_URL";
350
+ /**
351
+ * Whether a resolved catalog is the built-in one, which is what a `models`
352
+ * setting nobody customized resolves to.
353
+ * @param models - catalog resolved by {@link resolveAdapterOptions}.
354
+ * @returns whether `models` equals the resolved built-in catalog, entry by entry and in order.
355
+ */
356
+ function isBuiltInCatalog(models) {
357
+ return deepEqualJson(models, resolveModels(DEFAULT_MODELS));
358
+ }
359
+ /** Resolve, validate, and detach the advisory model catalog. */
360
+ function resolveModels(models) {
361
+ const seen = /* @__PURE__ */ new Set();
362
+ return (models ?? DEFAULT_MODELS).map((model) => {
363
+ if (Object.hasOwn(model, "imageDetail")) throw new Error("llm-deepseek: catalog model imageDetail is no longer supported; use imagePixelBudget");
364
+ if (model.id.length === 0) throw new Error("llm-deepseek: catalog model ids must be non-empty");
365
+ if (model.name !== void 0 && model.name.length === 0) throw new Error(`llm-deepseek: catalog model "${model.id}" has an empty name`);
366
+ if (model.contextWindow !== void 0 && (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) throw new Error(`llm-deepseek: catalog model "${model.id}" contextWindow must be a positive integer`);
367
+ if (model.maxTokens !== void 0 && (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) throw new Error(`llm-deepseek: catalog model "${model.id}" maxTokens must be a positive integer`);
368
+ const inputModalities = model.inputModalities ?? ["text"];
369
+ if (inputModalities.length === 0) throw new Error(`llm-deepseek: catalog model "${model.id}" inputModalities must not be empty`);
370
+ if (inputModalities.some((modality) => !MODEL_MODALITIES.includes(modality))) throw new Error(`llm-deepseek: catalog model "${model.id}" inputModalities must contain only "text" and "image"`);
371
+ if (new Set(inputModalities).size !== inputModalities.length) throw new Error(`llm-deepseek: catalog model "${model.id}" inputModalities must not contain duplicates`);
372
+ const hasImage = inputModalities.includes("image");
373
+ if (!hasImage && (model.imagePixelBudget !== void 0 || model.imageMaxBytes !== void 0)) throw new Error(`llm-deepseek: text-only catalog model "${model.id}" cannot declare image request limits`);
374
+ if (model.imagePixelBudget !== void 0 && model.imagePixelBudget !== "low" && (!Number.isSafeInteger(model.imagePixelBudget) || model.imagePixelBudget <= 0)) throw new Error(`llm-deepseek: catalog model "${model.id}" imagePixelBudget must be "low" or a positive safe integer`);
375
+ if (model.imageMaxBytes !== void 0 && (!Number.isSafeInteger(model.imageMaxBytes) || model.imageMaxBytes <= 0)) throw new Error(`llm-deepseek: catalog model "${model.id}" imageMaxBytes must be a positive safe integer`);
376
+ const systemPromptUpdate = model.systemPromptUpdate;
377
+ if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new Error(`llm-deepseek: catalog model "${model.id}" systemPromptUpdate must be "in-history" when present`);
378
+ const toolUpdate = model.toolUpdate;
379
+ if (toolUpdate !== void 0 && toolUpdate !== "in-history" && toolUpdate !== "addition-only") throw new Error(`llm-deepseek: catalog model "${model.id}" toolUpdate must be "in-history" or "addition-only" when present`);
380
+ if (seen.has(model.id)) throw new Error(`llm-deepseek: duplicate catalog model "${model.id}"`);
381
+ seen.add(model.id);
382
+ return {
383
+ id: model.id,
384
+ ...model.name === void 0 ? {} : { name: model.name },
385
+ ...model.description === void 0 ? {} : { description: model.description },
386
+ ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
387
+ ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
388
+ ...model.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: model.systemPromptUpdate },
389
+ ...model.toolUpdate === void 0 ? {} : { toolUpdate: model.toolUpdate },
390
+ inputModalities: [...inputModalities],
391
+ ...hasImage ? {
392
+ ...model.imagePixelBudget === void 0 ? {} : { imagePixelBudget: model.imagePixelBudget },
393
+ imageMaxBytes: model.imageMaxBytes ?? 2097152
394
+ } : {}
395
+ };
396
+ });
397
+ }
398
+ /**
399
+ * The one explicit resolve step from raw config to validated protocol
400
+ * settings. Programmatic construction may bypass Schemastery normalization, so
401
+ * every default and bound is re-judged here — for the composition entry at
402
+ * load (fail loud) and for each settings snapshot at its first use.
403
+ * @param config - raw plugin config or resolved settings snapshot.
404
+ * @param environment - this run's environment layers, or `undefined` outside
405
+ * the product CLI. Every layer may supply an endpoint: the product trusts the
406
+ * project it is launched in, so a checkout can point its own agent at the
407
+ * gateway that checkout is meant to use.
408
+ * @returns validated protocol settings; `models` holds one resolved entry per
409
+ * input model in input order, or the built-in catalog when `models` is absent.
410
+ */
411
+ function resolveAdapterOptions(config, environment) {
412
+ if (Object.hasOwn(config, "protocol")) throw new Error("llm-deepseek: protocol is not configurable; remove it and use a Messages-compatible baseURL");
413
+ if (config.thinking === "disabled" && config.reasoningEffort !== void 0 && config.reasoningEffort !== "off") throw new Error("llm-deepseek: only reasoningEffort \"off\" can be configured when thinking is disabled");
414
+ if (config.defaultContextWindow !== void 0 && (!Number.isInteger(config.defaultContextWindow) || config.defaultContextWindow <= 0)) throw new Error("llm-deepseek: defaultContextWindow must be a positive integer");
415
+ if (config.maxTokens !== void 0 && (!Number.isSafeInteger(config.maxTokens) || config.maxTokens <= 0)) throw new Error("llm-deepseek: maxTokens must be a positive safe integer");
416
+ const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? 3e5;
417
+ if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) throw new Error(`llm-deepseek: streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
418
+ const maxRequestFilesBytes = config.maxRequestFilesBytes ?? 134217728;
419
+ if (!Number.isSafeInteger(maxRequestFilesBytes) || maxRequestFilesBytes <= 0) throw new Error("llm-deepseek: maxRequestFilesBytes must be a positive safe integer");
420
+ const maxInlineRequestImageBytes = config.maxInlineRequestImageBytes ?? 20971520;
421
+ if (!Number.isSafeInteger(maxInlineRequestImageBytes) || maxInlineRequestImageBytes <= 0) throw new Error("llm-deepseek: maxInlineRequestImageBytes must be a positive safe integer");
422
+ const maxImagesPerRequest = config.maxImagesPerRequest ?? 600;
423
+ if (!Number.isSafeInteger(maxImagesPerRequest) || maxImagesPerRequest <= 0) throw new Error("llm-deepseek: maxImagesPerRequest must be a positive safe integer");
424
+ const imageOffloadByteQuantum = config.imageOffloadByteQuantum ?? 67108864;
425
+ if (!Number.isSafeInteger(imageOffloadByteQuantum) || imageOffloadByteQuantum <= 0) throw new Error("llm-deepseek: imageOffloadByteQuantum must be a positive safe integer");
426
+ if (imageOffloadByteQuantum > maxRequestFilesBytes) throw new Error("llm-deepseek: imageOffloadByteQuantum must not exceed maxRequestFilesBytes");
427
+ const inlineImageOffloadByteQuantum = config.inlineImageOffloadByteQuantum ?? 10485760;
428
+ if (!Number.isSafeInteger(inlineImageOffloadByteQuantum) || inlineImageOffloadByteQuantum <= 0) throw new Error("llm-deepseek: inlineImageOffloadByteQuantum must be a positive safe integer");
429
+ if (inlineImageOffloadByteQuantum > maxInlineRequestImageBytes) throw new Error("llm-deepseek: inlineImageOffloadByteQuantum must not exceed maxInlineRequestImageBytes");
430
+ const imageOffloadCountQuantum = config.imageOffloadCountQuantum ?? 20;
431
+ if (!Number.isSafeInteger(imageOffloadCountQuantum) || imageOffloadCountQuantum <= 0) throw new Error("llm-deepseek: imageOffloadCountQuantum must be a positive safe integer");
432
+ if (imageOffloadCountQuantum > maxImagesPerRequest) throw new Error("llm-deepseek: imageOffloadCountQuantum must not exceed maxImagesPerRequest");
433
+ const filesApiTimeoutMs = config.filesApiTimeoutMs ?? 6e4;
434
+ if (!Number.isFinite(filesApiTimeoutMs) || filesApiTimeoutMs <= 0 || filesApiTimeoutMs > MAX_TIMER_DELAY_MS) throw new Error(`llm-deepseek: filesApiTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
435
+ const fileExpiresAfterSeconds = config.fileExpiresAfterSeconds ?? 604800;
436
+ if (!Number.isSafeInteger(fileExpiresAfterSeconds) || fileExpiresAfterSeconds < 3600 || fileExpiresAfterSeconds > 2592e3) throw new Error("llm-deepseek: fileExpiresAfterSeconds must be an integer from 3600 through 2592000");
437
+ const fileRefreshMarginSeconds = config.fileRefreshMarginSeconds ?? 3600;
438
+ if (!Number.isSafeInteger(fileRefreshMarginSeconds) || fileRefreshMarginSeconds < 0 || fileRefreshMarginSeconds >= fileExpiresAfterSeconds) throw new Error("llm-deepseek: fileRefreshMarginSeconds must be a non-negative integer below fileExpiresAfterSeconds");
439
+ const fileQuotaCleanupBatch = config.fileQuotaCleanupBatch ?? 100;
440
+ if (!Number.isSafeInteger(fileQuotaCleanupBatch) || fileQuotaCleanupBatch < 1 || fileQuotaCleanupBatch > 1e3) throw new Error("llm-deepseek: fileQuotaCleanupBatch must be an integer from 1 through 1000");
441
+ const baseURL = config.baseURL ?? environment?.get(BASE_URL_ENV)?.value ?? "https://api.deepseek.com/anthropic";
442
+ const parsed = new URL(baseURL);
443
+ if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) throw new Error("llm-deepseek: Messages baseURL must be an HTTP(S) root without credentials, query, or fragment");
444
+ return {
445
+ baseURL,
446
+ defaults: {
447
+ thinking: config.thinking,
448
+ reasoningEffort: config.reasoningEffort
449
+ },
450
+ maxTokens: config.maxTokens ?? 256e3,
451
+ defaultContextWindow: config.defaultContextWindow ?? 1e6,
452
+ models: resolveModels(config.models),
453
+ streamIdleTimeoutMs,
454
+ maxRequestFilesBytes,
455
+ maxInlineRequestImageBytes,
456
+ maxImagesPerRequest,
457
+ imageOffloadByteQuantum,
458
+ inlineImageOffloadByteQuantum,
459
+ imageOffloadCountQuantum,
460
+ filesApiTimeoutMs,
461
+ filePolicy: {
462
+ expiresAfterSeconds: fileExpiresAfterSeconds,
463
+ refreshMarginSeconds: fileRefreshMarginSeconds,
464
+ quotaCleanupBatch: fileQuotaCleanupBatch
465
+ },
466
+ retryPolicy: resolveRetryPolicy(config.retryPolicy, "llm-deepseek: retryPolicy")
467
+ };
468
+ }
469
+ //#endregion
470
+ //#region lib/types/model-info.js
471
+ /** Protocol-independent model capabilities and reasoning choices. */
472
+ const OFF_REASONING_EFFORT = ReasoningEffortId("off");
473
+ const LOW_REASONING_EFFORT = ReasoningEffortId("low");
474
+ const HIGH_REASONING_EFFORT = ReasoningEffortId("high");
475
+ const MAX_REASONING_EFFORT = ReasoningEffortId("max");
476
+ const REASONING_EFFORTS = [
477
+ {
478
+ id: OFF_REASONING_EFFORT,
479
+ name: "Off",
480
+ description: "Use for simple tasks that do not need reasoning."
481
+ },
482
+ {
483
+ id: LOW_REASONING_EFFORT,
484
+ name: "Low",
485
+ description: "Prefer for routine or latency-sensitive tasks."
486
+ },
487
+ {
488
+ id: HIGH_REASONING_EFFORT,
489
+ name: "High",
490
+ description: "The default balance for most tasks."
491
+ },
492
+ {
493
+ id: MAX_REASONING_EFFORT,
494
+ name: "Max",
495
+ description: "Reserve for the hardest quality-first tasks."
496
+ }
497
+ ];
498
+ const OFF_ONLY_REASONING_EFFORTS = [{
499
+ id: OFF_REASONING_EFFORT,
500
+ name: "Off",
501
+ description: "Use for simple tasks that do not need reasoning."
502
+ }];
503
+ /** Advertise one catalog entry.
504
+ * @param provider - registered provider id.
505
+ * @param model - advisory catalog entry.
506
+ * @param badge - selector badge a model listing declares for the entry.
507
+ * @returns selector metadata.
508
+ */
509
+ function catalogModelInfo(provider, model, badge) {
510
+ return {
511
+ provider,
512
+ id: model.id,
513
+ name: model.name ?? model.id,
514
+ ...model.description === void 0 ? {} : { description: model.description },
515
+ inputModalities: model.inputModalities ?? ["text"],
516
+ ...badge === void 0 ? {} : { badge }
517
+ };
518
+ }
519
+ /** Resolve model capabilities against one configuration generation.
520
+ * @param connection - validated connection facts.
521
+ * @param provider - registered provider id.
522
+ * @param model - requested wire model id.
523
+ * @returns effective model metadata for this operation.
524
+ */
525
+ function modelInfo(connection, provider, model) {
526
+ const configured = connection.models.find((entry) => entry.id === model);
527
+ const contextWindow = configured?.contextWindow ?? connection.defaultContextWindow;
528
+ return {
529
+ ...configured === void 0 ? {
530
+ provider,
531
+ id: model,
532
+ name: model,
533
+ inputModalities: ["text"]
534
+ } : catalogModelInfo(provider, configured),
535
+ context: { contextWindow },
536
+ defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens,
537
+ ...configured?.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: configured.systemPromptUpdate },
538
+ ...configured?.toolUpdate === void 0 ? {} : { toolUpdate: configured.toolUpdate },
539
+ ...connection.defaults.thinking === "disabled" ? { reasoning: {
540
+ efforts: OFF_ONLY_REASONING_EFFORTS,
541
+ defaultEffort: OFF_REASONING_EFFORT
542
+ } } : { reasoning: {
543
+ efforts: REASONING_EFFORTS,
544
+ defaultEffort: connection.defaults.reasoningEffort === "off" ? OFF_REASONING_EFFORT : connection.defaults.reasoningEffort === "low" ? LOW_REASONING_EFFORT : connection.defaults.reasoningEffort === "max" ? MAX_REASONING_EFFORT : HIGH_REASONING_EFFORT
545
+ } }
546
+ };
547
+ }
548
+ //#endregion
549
+ //#region lib/types/file-id.js
550
+ /** DeepSeek Files API identifiers. @module dsh-llm-deepseek/file-id */
551
+ /**
552
+ * Brand a provider-returned file identifier after wire validation.
553
+ * @param id - non-empty Files API identifier.
554
+ * @returns the same string with its provider identity attached at type level.
555
+ */
556
+ function DeepSeekFileId(id) {
557
+ return id;
558
+ }
559
+ /**
560
+ * Brand a locally derived namespace digest.
561
+ * @param scope - SHA-256 digest of endpoint and API key.
562
+ * @returns the same string with namespace identity attached at type level.
563
+ */
564
+ function DeepSeekFileScope(scope) {
565
+ return scope;
566
+ }
567
+ //#endregion
568
+ //#region lib/types/messages-api.js
569
+ /** Shared DeepSeek Messages API endpoint and header policy. @module dsh-llm-deepseek/messages-api */
570
+ /** Required opt-in for Messages file operations and file-referenced image requests. */
571
+ const MESSAGES_FILES_BETA = "files-api-2025-04-14";
572
+ /** Required opt-in for mid-conversation `tool_addition` and `tool_removal` blocks. */
573
+ const MESSAGES_TOOL_CHANGES_BETA = "mid-conversation-tool-changes-2026-07-01";
574
+ /**
575
+ * Resolve the API root without duplicating an explicit provider version path.
576
+ * @param baseURL - validated configured endpoint root.
577
+ * @returns the root beneath which Messages resources are exposed.
578
+ */
579
+ function messagesApiRoot(baseURL) {
580
+ const base = baseURL.replace(/\/+$/u, "");
581
+ return new URL(base).pathname.endsWith("/v1") ? base : `${base}/v1`;
582
+ }
583
+ //#endregion
584
+ //#region lib/types/files-api.js
585
+ /** DeepSeek Files API transport. @module dsh-llm-deepseek/files-api */
586
+ /** Minimum provider-supported file lifetime. */
587
+ const MIN_FILE_EXPIRY_SECONDS = 3600;
588
+ /** Maximum provider-supported file lifetime. */
589
+ const MAX_FILE_EXPIRY_SECONDS = 2592e3;
590
+ /** Maximum Files API upload size. */
591
+ const MAX_FILE_UPLOAD_BYTES = 128 * 1024 * 1024;
592
+ /** Current per-key file-count quota. */
593
+ const MAX_STORED_FILE_COUNT = 1e4;
594
+ /** Current per-key storage quota. */
595
+ const MAX_STORED_FILE_BYTES = 25 * 1024 * 1024 * 1024;
596
+ /** Files API operation failure with its HTTP status retained for recovery policy. */
597
+ var DeepSeekFilesError = class extends LlmError {
598
+ /** Parsed provider detail used only for error classification. */
599
+ detail;
600
+ /**
601
+ * @param message - user-readable provider failure.
602
+ * @param status - HTTP status returned by the Files API.
603
+ * @param detail - provider error fields joined for classification.
604
+ */
605
+ constructor(message, status, detail) {
606
+ super(message, status === 401 || status === 403 ? "AUTH" : status === 429 ? "RATE_LIMIT" : status >= 500 ? "SERVER" : "FILES_API", { status });
607
+ this.name = "DeepSeekFilesError";
608
+ this.detail = detail;
609
+ }
610
+ };
611
+ /**
612
+ * Whether an upload failure reports a provider storage or file-count quota.
613
+ * @param error - Files API operation failure.
614
+ * @returns whether one bounded remote cleanup and upload retry may recover.
615
+ */
616
+ function isFilesQuotaError(error) {
617
+ return error instanceof DeepSeekFilesError && /(?:quota|storage|stored files|file count|too many files)/iu.test(error.detail);
618
+ }
619
+ function invalidResponse(operation) {
620
+ return new LlmError(`DeepSeek Files API returned an invalid ${operation} response.`, "INVALID_RESPONSE");
621
+ }
622
+ /** Decode successful Files JSON with operation context; body transport and abort failures retain their identity. */
623
+ async function responseJson(response, operation) {
624
+ try {
625
+ return await response.json();
626
+ } catch (error) {
627
+ if (!(error instanceof SyntaxError)) throw error;
628
+ throw new LlmError(`DeepSeek Files API returned invalid JSON for ${operation} (HTTP ${response.status}).`, "INVALID_RESPONSE", {
629
+ status: response.status,
630
+ cause: error
631
+ });
632
+ }
633
+ }
634
+ function parseFileObject(value, operation) {
635
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidResponse(operation);
636
+ const wire = value;
637
+ const createdAt = typeof wire.created_at === "string" ? Math.floor(Date.parse(wire.created_at) / 1e3) : NaN;
638
+ if (typeof wire.id !== "string" || wire.id.length === 0 || wire.type !== "file" || typeof wire.mime_type !== "string" || typeof wire.size_bytes !== "number" || !Number.isSafeInteger(wire.size_bytes) || wire.size_bytes < 0 || !Number.isSafeInteger(createdAt) || createdAt < 0 || typeof wire.filename !== "string" || wire.filename.length === 0) throw invalidResponse(operation);
639
+ return {
640
+ id: DeepSeekFileId(wire.id),
641
+ bytes: wire.size_bytes,
642
+ createdAt,
643
+ filename: wire.filename
644
+ };
645
+ }
646
+ function providerErrorDetail$1(value) {
647
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return { detail: "" };
648
+ const error = value.error;
649
+ if (error === null || typeof error !== "object" || Array.isArray(error)) return { detail: "" };
650
+ const fields = error;
651
+ const message = typeof fields.message === "string" ? fields.message : void 0;
652
+ return {
653
+ ...message === void 0 ? {} : { message },
654
+ detail: [
655
+ fields.code,
656
+ fields.type,
657
+ fields.message
658
+ ].filter((field) => typeof field === "string").join(" ")
659
+ };
660
+ }
661
+ /** Direct Files client retaining the configured URL root and refusing redirects before credentials can leave its origin. */
662
+ var DeepSeekFilesClient = class {
663
+ baseURL;
664
+ authHeaders;
665
+ fetchImpl;
666
+ /**
667
+ * @param options - endpoint, authentication headers, and optional test transport.
668
+ */
669
+ constructor(options) {
670
+ this.authHeaders = options.headers;
671
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
672
+ this.baseURL = messagesApiRoot(options.baseURL);
673
+ }
674
+ async request(path, init, signal) {
675
+ let response;
676
+ try {
677
+ const headers = new Headers(attributionHeaders());
678
+ for (const [name, value] of Object.entries(this.authHeaders)) headers.set(name, value);
679
+ headers.set("anthropic-version", "2023-06-01");
680
+ headers.set("anthropic-beta", MESSAGES_FILES_BETA);
681
+ response = await this.fetchImpl(`${this.baseURL}${path}`, {
682
+ ...init,
683
+ redirect: "error",
684
+ headers,
685
+ ...signal === void 0 ? {} : { signal }
686
+ });
687
+ } catch (error) {
688
+ if (signal?.aborted) throw error;
689
+ throw new LlmError(`DeepSeek Files API request to ${this.baseURL} failed`, "TRANSPORT", { cause: error });
690
+ }
691
+ if (response.ok) return response;
692
+ let parsed;
693
+ try {
694
+ parsed = await response.json();
695
+ } catch {}
696
+ const { message, detail } = providerErrorDetail$1(parsed);
697
+ throw new DeepSeekFilesError(message ?? `DeepSeek Files API error (HTTP ${response.status})`, response.status, detail);
698
+ }
699
+ /**
700
+ * Upload one image with an explicit expiry.
701
+ * @param input - deterministic request-version bytes, media type, filename, lifetime, and cancellation.
702
+ * @returns the validated file and reuse deadline. Messages omits expiry metadata;
703
+ * its deadline uses upload creation plus the requested lifetime.
704
+ */
705
+ async upload(input) {
706
+ if (input.data.byteLength > 134217728) throw new LlmError("DeepSeek Files API upload exceeds 128 MiB.", "INVALID_REQUEST");
707
+ if (!Number.isSafeInteger(input.expiresAfterSeconds) || input.expiresAfterSeconds < 3600 || input.expiresAfterSeconds > 2592e3) throw new LlmError("DeepSeek file expiry must be between 3600 and 2592000 seconds.", "INVALID_REQUEST");
708
+ const form = new FormData();
709
+ form.set("expires_after[anchor]", "created_at");
710
+ form.set("expires_after[seconds]", String(input.expiresAfterSeconds));
711
+ form.set("file", new Blob([Uint8Array.from(input.data).buffer], { type: input.mediaType }), input.filename);
712
+ const file = parseFileObject(await responseJson(await this.request("/files", {
713
+ method: "POST",
714
+ body: form
715
+ }, input.signal), "upload"), "upload");
716
+ return {
717
+ ...file,
718
+ expiresAt: file.createdAt + input.expiresAfterSeconds
719
+ };
720
+ }
721
+ /**
722
+ * List one provider-ordered page of files.
723
+ * @param options - pagination and cancellation.
724
+ * @returns the validated page with null cursors omitted.
725
+ */
726
+ async list(options = {}) {
727
+ const query = new URLSearchParams();
728
+ if (options.after !== void 0) query.set("after_id", options.after);
729
+ if (options.limit !== void 0) query.set("limit", String(options.limit));
730
+ const value = await responseJson(await this.request(`/files?${query.toString()}`, { method: "GET" }, options.signal), "list");
731
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidResponse("list");
732
+ const wire = value;
733
+ const firstId = wire.first_id ?? void 0;
734
+ const lastId = wire.last_id ?? void 0;
735
+ if (!Array.isArray(wire.data) || typeof wire.has_more !== "boolean" || firstId !== void 0 && typeof firstId !== "string" || lastId !== void 0 && typeof lastId !== "string") throw invalidResponse("list");
736
+ return {
737
+ data: wire.data.map((item) => parseFileObject(item, "list")),
738
+ ...typeof firstId === "string" ? { firstId: DeepSeekFileId(firstId) } : {},
739
+ ...typeof lastId === "string" ? { lastId: DeepSeekFileId(lastId) } : {},
740
+ hasMore: wire.has_more
741
+ };
742
+ }
743
+ /**
744
+ * Retrieve one file object.
745
+ * @param fileId - provider file identifier.
746
+ * @param signal - request cancellation.
747
+ * @returns the validated file object.
748
+ */
749
+ async retrieve(fileId, signal) {
750
+ return parseFileObject(await responseJson(await this.request(`/files/${encodeURIComponent(fileId)}`, { method: "GET" }, signal), "retrieve"), "retrieve");
751
+ }
752
+ /**
753
+ * Delete one provider file.
754
+ * @param fileId - provider file identifier.
755
+ * @param signal - request cancellation.
756
+ */
757
+ async delete(fileId, signal) {
758
+ const value = await responseJson(await this.request(`/files/${encodeURIComponent(fileId)}`, { method: "DELETE" }, signal), "delete");
759
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidResponse("delete");
760
+ const wire = value;
761
+ if (wire.id !== fileId || wire.type !== "file_deleted") throw invalidResponse("delete");
762
+ }
763
+ };
764
+ //#endregion
765
+ //#region lib/types/upload-index.js
766
+ /** Durable DeepSeek attachment-to-file-id index. @module dsh-llm-deepseek/upload-index */
767
+ var InvalidUploadIndexError = class extends Error {};
768
+ /**
769
+ * Derive a non-secret stable index namespace without persisting or logging authentication headers.
770
+ * @param baseURL - normalized provider endpoint namespace.
771
+ * @param credentials - serialized authentication headers used only as hash input.
772
+ * @returns branded SHA-256 namespace digest.
773
+ */
774
+ function deepSeekFileScope(baseURL, credentials) {
775
+ return DeepSeekFileScope(createHash("sha256").update(baseURL.replace(/\/+$/u, "")).update("\0").update(credentials).digest("hex"));
776
+ }
777
+ function absent(error) {
778
+ return error?.code === "ENOENT";
779
+ }
780
+ function parseRecord(value) {
781
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new InvalidUploadIndexError("llm-deepseek: upload index contains a non-object record");
782
+ const record = value;
783
+ if (typeof record.scope !== "string" || !/^[0-9a-f]{64}$/u.test(record.scope) || typeof record.attachmentId !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(record.attachmentId) || typeof record.variantId !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(record.variantId) || typeof record.fileId !== "string" || record.fileId.length === 0 || !Number.isSafeInteger(record.bytes) || record.bytes < 0 || !Number.isSafeInteger(record.createdAt) || record.createdAt < 0 || !Number.isSafeInteger(record.expiresAt) || record.expiresAt < 0) throw new InvalidUploadIndexError("llm-deepseek: upload index contains an invalid record");
784
+ return {
785
+ scope: DeepSeekFileScope(record.scope),
786
+ attachmentId: record.attachmentId,
787
+ variantId: ImageVariantId(record.variantId),
788
+ fileId: DeepSeekFileId(record.fileId),
789
+ bytes: record.bytes,
790
+ createdAt: record.createdAt,
791
+ expiresAt: record.expiresAt
792
+ };
793
+ }
794
+ function parseIndex(text) {
795
+ let value;
796
+ try {
797
+ value = JSON.parse(text);
798
+ } catch (error) {
799
+ throw new InvalidUploadIndexError("llm-deepseek: upload index is not valid JSON", { cause: error });
800
+ }
801
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new InvalidUploadIndexError("llm-deepseek: upload index is not an object");
802
+ const index = value;
803
+ if (index.formatVersion !== 3 || !Array.isArray(index.records)) throw new InvalidUploadIndexError("llm-deepseek: unsupported upload index format");
804
+ const records = index.records.map(parseRecord);
805
+ const keys = /* @__PURE__ */ new Set();
806
+ for (const record of records) {
807
+ const key = `${record.scope}\0${record.variantId}`;
808
+ if (keys.has(key)) throw new InvalidUploadIndexError("llm-deepseek: upload index contains duplicate mappings");
809
+ keys.add(key);
810
+ }
811
+ return {
812
+ formatVersion: 3,
813
+ records
814
+ };
815
+ }
816
+ function reusable(record, now, refreshMarginMs) {
817
+ return record.expiresAt - now > refreshMarginMs;
818
+ }
819
+ /** Atomic local index shared by every DeepSeek session in this DSH home. */
820
+ var DeepSeekUploadIndex = class {
821
+ /** Absolute owner-private JSON index path. */
822
+ path;
823
+ /**
824
+ * @param path - explicit test path; omission uses `DSH_HOME/llm-deepseek/files-v3.json`.
825
+ */
826
+ constructor(path = join(resolveDshHome(), "llm-deepseek", "files-v3.json")) {
827
+ this.path = path;
828
+ }
829
+ async load() {
830
+ try {
831
+ return parseIndex(await readFile(this.path, "utf8"));
832
+ } catch (error) {
833
+ if (absent(error) || error instanceof InvalidUploadIndexError) return {
834
+ formatVersion: 3,
835
+ records: []
836
+ };
837
+ throw error;
838
+ }
839
+ }
840
+ async save(index) {
841
+ await writeFileAtomic(this.path, `${JSON.stringify(index, void 0, 2)}\n`, {
842
+ mode: 384,
843
+ dirMode: 448
844
+ });
845
+ }
846
+ /**
847
+ * Read one reusable mapping.
848
+ * @param scope - endpoint/API-key namespace.
849
+ * @param variantId - complete request-image transformation identity.
850
+ * @param now - current Unix time in milliseconds.
851
+ * @param refreshMarginMs - remaining lifetime below which a mapping is not reused.
852
+ * @returns the mapping when it has enough lifetime remaining.
853
+ */
854
+ async get(scope, variantId, now, refreshMarginMs) {
855
+ const record = (await this.load()).records.find((candidate) => candidate.scope === scope && candidate.variantId === variantId);
856
+ return record !== void 0 && reusable(record, now, refreshMarginMs) ? record : void 0;
857
+ }
858
+ /**
859
+ * Publish a completed upload unless another process already published a reusable mapping.
860
+ * @param candidate - completed remote upload.
861
+ * @param now - current Unix time in milliseconds.
862
+ * @param refreshMarginMs - minimum reusable remaining lifetime.
863
+ * @returns the winning record and whether the candidate entered the index.
864
+ */
865
+ async commit(candidate, now, refreshMarginMs) {
866
+ await mkdir(dirname(this.path), {
867
+ recursive: true,
868
+ mode: 448
869
+ });
870
+ return withFileLock(this.path, async () => {
871
+ const index = await this.load();
872
+ const existing = index.records.find((record) => record.scope === candidate.scope && record.variantId === candidate.variantId && reusable(record, now, refreshMarginMs));
873
+ if (existing !== void 0) return {
874
+ record: existing,
875
+ accepted: false
876
+ };
877
+ const records = index.records.filter((record) => reusable(record, now, refreshMarginMs) && !(record.scope === candidate.scope && record.variantId === candidate.variantId));
878
+ records.push(candidate);
879
+ await this.save({
880
+ formatVersion: 3,
881
+ records
882
+ });
883
+ return {
884
+ record: candidate,
885
+ accepted: true
886
+ };
887
+ });
888
+ }
889
+ /**
890
+ * Remove exact mappings in one locked rewrite without deleting concurrently installed successors.
891
+ * @param scope - endpoint/API-key namespace.
892
+ * @param generations - exact remote generations being invalidated; pairs absent from the index are ignored.
893
+ */
894
+ async remove(scope, generations) {
895
+ const invalidated = new Set(generations.map((generation) => `${generation.variantId}\0${generation.fileId}`));
896
+ await mkdir(dirname(this.path), {
897
+ recursive: true,
898
+ mode: 448
899
+ });
900
+ await withFileLock(this.path, async () => {
901
+ const index = await this.load();
902
+ const records = index.records.filter((record) => !(record.scope === scope && invalidated.has(`${record.variantId}\0${record.fileId}`)));
903
+ if (records.length !== index.records.length) await this.save({
904
+ formatVersion: 3,
905
+ records
906
+ });
907
+ });
908
+ }
909
+ /**
910
+ * Remove every local mapping for one remote namespace.
911
+ * @param scope - endpoint/API-key namespace.
912
+ */
913
+ async clear(scope) {
914
+ await mkdir(dirname(this.path), {
915
+ recursive: true,
916
+ mode: 448
917
+ });
918
+ await withFileLock(this.path, async () => {
919
+ const index = await this.load();
920
+ const records = index.records.filter((record) => record.scope !== scope);
921
+ if (records.length !== index.records.length) await this.save({
922
+ formatVersion: 3,
923
+ records
924
+ });
925
+ });
926
+ }
927
+ };
928
+ //#endregion
929
+ //#region lib/types/file-store.js
930
+ /** DeepSeek Files API upload reuse, invalidation, and quota recovery. @module dsh-llm-deepseek/file-store */
931
+ /** Shared Files-store limit for each request image, including file-id references. */
932
+ const MAX_IMAGE_BYTES = 32 * 1024 * 1024;
933
+ const OWNED_FILE_PREFIX = "dsh-";
934
+ /** The Files resource's parent URL identifies the upload namespace. */
935
+ function fileScope(connection) {
936
+ return deepSeekFileScope(messagesApiRoot(connection.baseURL), JSON.stringify(Object.entries(connection.headers).sort(([left], [right]) => left.localeCompare(right))));
937
+ }
938
+ function abortReason(signal) {
939
+ const reason = signal.reason;
940
+ return reason instanceof Error ? reason : new Error("DeepSeek file upload cancelled with a non-Error reason.", { cause: reason });
941
+ }
942
+ function uploadFailure(error) {
943
+ return error instanceof Error ? error : new Error("DeepSeek file upload failed with a non-Error reason.", { cause: error });
944
+ }
945
+ function waitForUpload(operation, signal) {
946
+ signal?.throwIfAborted();
947
+ operation.waiters += 1;
948
+ let released = false;
949
+ const release = (cancelledReason) => {
950
+ if (released) return;
951
+ released = true;
952
+ operation.waiters -= 1;
953
+ if (cancelledReason !== void 0 && operation.waiters === 0 && !operation.settled) operation.controller.abort(cancelledReason);
954
+ };
955
+ if (signal === void 0) return operation.promise.finally(() => {
956
+ release();
957
+ });
958
+ return new Promise((resolve, reject) => {
959
+ const abort = () => {
960
+ const reason = abortReason(signal);
961
+ release(reason);
962
+ reject(reason);
963
+ };
964
+ signal.addEventListener("abort", abort, { once: true });
965
+ operation.promise.then((value) => {
966
+ signal.removeEventListener("abort", abort);
967
+ release();
968
+ resolve(value);
969
+ }, (error) => {
970
+ signal.removeEventListener("abort", abort);
971
+ release();
972
+ reject(uploadFailure(error));
973
+ });
974
+ });
975
+ }
976
+ function extension(mediaType) {
977
+ switch (mediaType) {
978
+ case "image/png": return "png";
979
+ case "image/jpeg": return "jpeg";
980
+ case "image/webp": return "webp";
981
+ case "image/gif": return "gif";
982
+ }
983
+ }
984
+ function filename(version) {
985
+ return `${OWNED_FILE_PREFIX}${String(version.attachment.attachmentId).slice(7, 23)}-${String(version.variantId).slice(7, 15)}.${extension(version.mediaType)}`;
986
+ }
987
+ /** User-scoped durable file-id reuse for the DeepSeek route. */
988
+ var DeepSeekFileStore = class {
989
+ index;
990
+ now;
991
+ fetchImpl;
992
+ inflight = /* @__PURE__ */ new Map();
993
+ /**
994
+ * @param options - testable index, clock, and transport boundaries.
995
+ */
996
+ constructor(options = {}) {
997
+ this.index = options.index ?? new DeepSeekUploadIndex();
998
+ this.now = options.now ?? Date.now;
999
+ this.fetchImpl = options.fetch;
1000
+ }
1001
+ client(connection) {
1002
+ return new DeepSeekFilesClient({
1003
+ baseURL: connection.baseURL,
1004
+ headers: connection.headers,
1005
+ ...this.fetchImpl === void 0 ? {} : { fetch: this.fetchImpl }
1006
+ });
1007
+ }
1008
+ /**
1009
+ * Resolve or upload one deterministic request image. Concurrent calls share one upload while retaining independent waits.
1010
+ * @param version - deterministic model-request bytes and complete transformation identity.
1011
+ * @param connection - endpoint and API-key snapshot.
1012
+ * @param policy - expiry and quota-recovery policy.
1013
+ * @param signal - cancellation of this wait; shared transport stops when no waiter remains.
1014
+ * @returns a reusable file id and whether this call published a new upload.
1015
+ */
1016
+ ensureUploaded(version, connection, policy, signal) {
1017
+ signal?.throwIfAborted();
1018
+ const key = `${fileScope(connection)}\0${version.variantId}`;
1019
+ let active = this.inflight.get(key);
1020
+ if (active?.controller.signal.aborted) {
1021
+ this.inflight.delete(key);
1022
+ active = void 0;
1023
+ }
1024
+ if (active !== void 0) return waitForUpload(active, signal);
1025
+ const controller = new AbortController();
1026
+ const shared = {
1027
+ controller,
1028
+ settled: false,
1029
+ waiters: 0,
1030
+ promise: void 0
1031
+ };
1032
+ shared.promise = this.ensureUploadedOnce(version, connection, policy, controller.signal).then((value) => {
1033
+ shared.settled = true;
1034
+ return value;
1035
+ }, (error) => {
1036
+ shared.settled = true;
1037
+ throw uploadFailure(error);
1038
+ });
1039
+ this.inflight.set(key, shared);
1040
+ shared.promise.finally(() => {
1041
+ if (this.inflight.get(key) === shared) this.inflight.delete(key);
1042
+ }).catch(() => {});
1043
+ return waitForUpload(shared, signal);
1044
+ }
1045
+ async ensureUploadedOnce(version, connection, policy, signal) {
1046
+ if (version.bytes > 33554432) throw new LlmError("DeepSeek image exceeds the 32 MiB per-image limit.", "INVALID_REQUEST");
1047
+ const scope = fileScope(connection);
1048
+ const now = this.now();
1049
+ const marginMs = policy.refreshMarginSeconds * 1e3;
1050
+ const cached = await this.index.get(scope, version.variantId, now, marginMs);
1051
+ if (cached !== void 0) return {
1052
+ record: cached,
1053
+ uploaded: false
1054
+ };
1055
+ const client = this.client(connection);
1056
+ const upload = async () => {
1057
+ const remote = await client.upload({
1058
+ data: version.data,
1059
+ mediaType: version.mediaType,
1060
+ filename: filename(version),
1061
+ expiresAfterSeconds: policy.expiresAfterSeconds,
1062
+ signal
1063
+ });
1064
+ if (remote.bytes !== version.data.byteLength) throw new LlmError("DeepSeek Files API upload response does not match the submitted image.", "INVALID_RESPONSE");
1065
+ return {
1066
+ scope,
1067
+ attachmentId: version.attachment.attachmentId,
1068
+ variantId: version.variantId,
1069
+ fileId: remote.id,
1070
+ bytes: remote.bytes,
1071
+ createdAt: remote.createdAt * 1e3,
1072
+ expiresAt: remote.expiresAt * 1e3
1073
+ };
1074
+ };
1075
+ let candidate;
1076
+ try {
1077
+ candidate = await upload();
1078
+ } catch (error) {
1079
+ if (!isFilesQuotaError(error)) throw error;
1080
+ if (await this.reclaimOldestOwned(connection, policy.quotaCleanupBatch, signal) === 0) throw error;
1081
+ candidate = await upload();
1082
+ }
1083
+ const committed = await this.index.commit(candidate, this.now(), marginMs);
1084
+ if (!committed.accepted) try {
1085
+ await client.delete(candidate.fileId, signal);
1086
+ } catch {}
1087
+ return {
1088
+ record: committed.record,
1089
+ uploaded: committed.accepted
1090
+ };
1091
+ }
1092
+ /**
1093
+ * Invalidate exact local mappings in one index update after a model request rejects their remote ids.
1094
+ * @param generations - request-image variants with the exact file id the request used for each.
1095
+ * @param connection - endpoint and API-key snapshot.
1096
+ */
1097
+ async invalidate(generations, connection) {
1098
+ await this.index.remove(fileScope(connection), generations);
1099
+ }
1100
+ /**
1101
+ * Delete the indexed remote file for one attachment and remove its local mapping.
1102
+ * @param version - exact request-image version to release.
1103
+ * @param connection - endpoint and API-key snapshot.
1104
+ * @param policy - expiry policy used to locate a reusable mapping.
1105
+ * @param signal - request cancellation.
1106
+ * @returns whether an indexed file existed and was deleted.
1107
+ */
1108
+ async release(version, connection, policy, signal) {
1109
+ const scope = fileScope(connection);
1110
+ const record = await this.index.get(scope, version.variantId, this.now(), policy.refreshMarginSeconds * 1e3);
1111
+ if (record === void 0) return false;
1112
+ await this.client(connection).delete(record.fileId, signal);
1113
+ await this.index.remove(scope, [{
1114
+ variantId: version.variantId,
1115
+ fileId: record.fileId
1116
+ }]);
1117
+ return true;
1118
+ }
1119
+ /**
1120
+ * Delete the oldest provider files whose names identify harness ownership.
1121
+ * @param connection - endpoint and API-key snapshot.
1122
+ * @param count - positive maximum number of files to delete.
1123
+ * @param signal - request cancellation.
1124
+ * @returns number of successfully deleted files.
1125
+ */
1126
+ async reclaimOldestOwned(connection, count, signal) {
1127
+ const client = this.client(connection);
1128
+ let after;
1129
+ const owned = [];
1130
+ while (true) {
1131
+ const page = await client.list({
1132
+ ...after === void 0 ? {} : { after },
1133
+ limit: 1e3,
1134
+ ...signal === void 0 ? {} : { signal }
1135
+ });
1136
+ for (const file of page.data) {
1137
+ if (!file.filename.startsWith(OWNED_FILE_PREFIX)) continue;
1138
+ owned.push({
1139
+ id: file.id,
1140
+ createdAt: file.createdAt
1141
+ });
1142
+ }
1143
+ owned.sort((left, right) => left.createdAt - right.createdAt);
1144
+ owned.splice(count);
1145
+ if (!page.hasMore || page.lastId === void 0 || page.lastId === after) break;
1146
+ after = page.lastId;
1147
+ }
1148
+ for (const file of owned) await client.delete(file.id, signal);
1149
+ return owned.length;
1150
+ }
1151
+ /**
1152
+ * Delete every remote harness-owned file in the active API-key namespace and clear its index.
1153
+ * @param connection - endpoint and API-key snapshot.
1154
+ * @param signal - request cancellation.
1155
+ * @returns number of deleted files.
1156
+ */
1157
+ async releaseAll(connection, signal) {
1158
+ let total = 0;
1159
+ for (;;) {
1160
+ const deleted = await this.reclaimOldestOwned(connection, 1e3, signal);
1161
+ total += deleted;
1162
+ if (deleted < 1e3) break;
1163
+ }
1164
+ await this.index.clear(fileScope(connection));
1165
+ return total;
1166
+ }
1167
+ };
1168
+ //#endregion
1169
+ //#region lib/types/request-files.js
1170
+ /** Shared Files resolution, bounded stale-id recovery, and normalized-image diagnostics. */
1171
+ var __addDisposableResource$1 = function(env, value, async) {
1172
+ if (value !== null && value !== void 0) {
1173
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
1174
+ var dispose, inner;
1175
+ if (async) {
1176
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
1177
+ dispose = value[Symbol.asyncDispose];
1178
+ }
1179
+ if (dispose === void 0) {
1180
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
1181
+ dispose = value[Symbol.dispose];
1182
+ if (async) inner = dispose;
1183
+ }
1184
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
1185
+ if (inner) dispose = function() {
1186
+ try {
1187
+ inner.call(this);
1188
+ } catch (e) {
1189
+ return Promise.reject(e);
1190
+ }
1191
+ };
1192
+ env.stack.push({
1193
+ value,
1194
+ dispose,
1195
+ async
1196
+ });
1197
+ } else if (async) env.stack.push({ async: true });
1198
+ return value;
1199
+ };
1200
+ var __disposeResources$1 = (function(SuppressedError) {
1201
+ return function(env) {
1202
+ function fail(e) {
1203
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
1204
+ env.hasError = true;
1205
+ }
1206
+ var r, s = 0;
1207
+ function next() {
1208
+ while (r = env.stack.pop()) try {
1209
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
1210
+ if (r.dispose) {
1211
+ var result = r.dispose.call(r.value);
1212
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
1213
+ fail(e);
1214
+ return next();
1215
+ });
1216
+ } else s |= 1;
1217
+ } catch (e) {
1218
+ fail(e);
1219
+ }
1220
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
1221
+ if (env.hasError) throw env.error;
1222
+ }
1223
+ return next();
1224
+ };
1225
+ })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
1226
+ var e = new Error(message);
1227
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1228
+ });
1229
+ /** A file upload failure eligible for request-wide inline fallback. */
1230
+ var FileResolutionFailure = class extends Error {
1231
+ constructor(cause) {
1232
+ super("DeepSeek Files API could not resolve a request image.", { cause });
1233
+ this.name = "FileResolutionFailure";
1234
+ }
1235
+ };
1236
+ function providerRejectedNormalizedImage(detail) {
1237
+ return /(?:unsupported|invalid|cannot read|failed to (?:decode|process)).{0,40}image/iu.test(detail) || /image.{0,40}(?:unsupported|invalid|cannot be decoded)/iu.test(detail);
1238
+ }
1239
+ function providerRejectedFileId(detail) {
1240
+ const file = /\bfile(?:[_ -]?(?:id|api|not[_ -]?found|deleted|expired))?/iu.test(detail);
1241
+ const missing = /(?:expired|not[_ -]?found|deleted|do(?:es)? not exist|not created under (?:this|your) account)/iu.test(detail);
1242
+ const invalidId = /(?:invalid.{0,20}file[_ -]?(?:id|api)|file[_ -]?(?:id|api).{0,20}invalid)/iu.test(detail);
1243
+ return file && (missing || invalidId);
1244
+ }
1245
+ function detailNamesFileId(detail, fileId) {
1246
+ let index = detail.indexOf(fileId);
1247
+ while (index >= 0) {
1248
+ const before = detail[index - 1];
1249
+ const after = detail[index + fileId.length];
1250
+ if ((before === void 0 || !/[\p{L}\p{N}_-]/u.test(before)) && (after === void 0 || !/[\p{L}\p{N}_-]/u.test(after))) return true;
1251
+ index = detail.indexOf(fileId, index + 1);
1252
+ }
1253
+ return false;
1254
+ }
1255
+ function staleMappings(files, detail) {
1256
+ const unique = [...new Map(files.map((file) => [`${file.version.variantId}\0${file.fileId}`, file])).values()];
1257
+ const exact = unique.filter((file) => detailNamesFileId(detail, file.fileId));
1258
+ return exact.length > 0 ? exact : unique;
1259
+ }
1260
+ function normalizedImageFacts(file) {
1261
+ const version = file.version;
1262
+ const name = version.attachment.name ?? version.attachment.attachmentId;
1263
+ const colour = version.hasAlpha ? "sRGBA" : "sRGB";
1264
+ return `"${name}" at message ${file.location.message}, image ${file.location.image} (${version.mediaType}, 8-bit ${colour}, ${version.width}x${version.height})`;
1265
+ }
1266
+ function normalizedImageDiagnostic(files, providerMessage, providerDetail) {
1267
+ const target = files.find((file) => detailNamesFileId(providerDetail, file.fileId)) ?? (files.length === 1 ? files[0] : void 0);
1268
+ if (target !== void 0) return `DeepSeek rejected normalized image ${normalizedImageFacts(target)}: ${providerMessage}. The provider rejected bytes already normalized by the harness; PNG, JPEG, WebP, and GIF remain supported input formats.`;
1269
+ return `DeepSeek rejected a normalized request image: ${providerMessage}. Candidate images: ${[...new Map(files.map((file) => [`${file.version.variantId}\0${file.location.message}\0${file.location.image}`, file])).values()].map(normalizedImageFacts).join("; ")}. The provider rejected bytes already normalized by the harness; PNG, JPEG, WebP, and GIF remain supported input formats.`;
1270
+ }
1271
+ /** Files state owned by one model request, including at most one stale-id retry. */
1272
+ var RequestFiles = class {
1273
+ files;
1274
+ connection;
1275
+ policy;
1276
+ timeoutMs;
1277
+ signal;
1278
+ activity;
1279
+ used = [];
1280
+ retried = false;
1281
+ constructor(files, connection, policy, timeoutMs, signal, activity) {
1282
+ this.files = files;
1283
+ this.connection = connection;
1284
+ this.policy = policy;
1285
+ this.timeoutMs = timeoutMs;
1286
+ this.signal = signal;
1287
+ this.activity = activity;
1288
+ }
1289
+ /** Reset occurrence tracking before serializing the next HTTP attempt. */
1290
+ beginAttempt() {
1291
+ this.used = [];
1292
+ }
1293
+ /**
1294
+ * Resolve a retained image under its own upload deadline.
1295
+ * @param version - prepared request image.
1296
+ * @param location - occurrence used by provider-rejection diagnostics.
1297
+ * @returns the reusable provider id.
1298
+ */
1299
+ async resolve(version, location) {
1300
+ const env_1 = {
1301
+ stack: [],
1302
+ error: void 0,
1303
+ hasError: false
1304
+ };
1305
+ try {
1306
+ const limit = __addDisposableResource$1(env_1, deadline(this.signal, this.timeoutMs, "DEEPSEEK_FILES_API_TIMEOUT"), false);
1307
+ let resolved;
1308
+ try {
1309
+ resolved = await this.files.ensureUploaded(version, this.connection, this.policy, limit.signal);
1310
+ } catch (error) {
1311
+ if (this.signal.aborted) throw error;
1312
+ throw new FileResolutionFailure(error);
1313
+ }
1314
+ this.activity();
1315
+ this.used.push({
1316
+ version,
1317
+ fileId: resolved.record.fileId,
1318
+ location
1319
+ });
1320
+ return resolved.record.fileId;
1321
+ } catch (e_1) {
1322
+ env_1.error = e_1;
1323
+ env_1.hasError = true;
1324
+ } finally {
1325
+ __disposeResources$1(env_1);
1326
+ }
1327
+ }
1328
+ /**
1329
+ * Invalidate every rejected mapping in one index update; only the first stale-id response permits another request.
1330
+ * @param detail - provider error fields used for stale-id classification.
1331
+ * @returns whether the caller should serialize and dispatch again.
1332
+ */
1333
+ async retry(detail) {
1334
+ if (this.used.length === 0 || !providerRejectedFileId(detail)) return false;
1335
+ await this.files.invalidate(staleMappings(this.used, detail).map((file) => ({
1336
+ variantId: file.version.variantId,
1337
+ fileId: file.fileId
1338
+ })), this.connection);
1339
+ if (this.retried) return false;
1340
+ this.retried = true;
1341
+ return true;
1342
+ }
1343
+ /**
1344
+ * Attribute a normalized-image rejection to the actual uploaded image occurrences.
1345
+ * @param status - rejected request's HTTP status.
1346
+ * @param message - provider's error message.
1347
+ * @param detail - provider error classification fields.
1348
+ * @returns the image diagnostic or the original provider message.
1349
+ */
1350
+ errorMessage(status, message, detail) {
1351
+ return status === 400 && this.used.length > 0 && providerRejectedNormalizedImage(detail) ? normalizedImageDiagnostic(this.used, message, detail) : message;
1352
+ }
1353
+ };
1354
+ //#endregion
1355
+ //#region lib/types/request-extensions.js
1356
+ /** Prepare plugin-contributed request fields and commit their delivery after HTTP acceptance. */
1357
+ /**
1358
+ * Merge contributions without replacing Messages fields. A rejected preparation or
1359
+ * a base-field collision reports REQUEST_EXTENSION with the cause chain appended to
1360
+ * the message. When the merged request fails to serialize, the payload is the
1361
+ * base request alone and acceptance is a no-op, so contributors resend their
1362
+ * unaccepted state on a later request. An acceptance failure is reported and
1363
+ * does not fail the request.
1364
+ * @param body - serialized Messages request before extension fields.
1365
+ * @param options - request identity, purpose, and cancellation.
1366
+ * @param prepare - contributor registry captured for this adapter.
1367
+ * @param onOmitted - receives the omitted field names and the serialization failure.
1368
+ * @param onUnaccepted - receives an acceptance failure after HTTP 2xx.
1369
+ * @returns HTTP payload and a commit to invoke only after a successful HTTP response.
1370
+ */
1371
+ async function prepareRequestExtensions(body, options, prepare, onOmitted, onUnaccepted) {
1372
+ let extensions;
1373
+ try {
1374
+ extensions = await prepare({
1375
+ body,
1376
+ ...options
1377
+ });
1378
+ } catch (error) {
1379
+ throw new LlmError(`DeepSeek request extension preparation failed: ${errorChain(error)}`, "REQUEST_EXTENSION", { cause: error });
1380
+ }
1381
+ const fields = Object.keys(extensions.fields);
1382
+ for (const field of fields) if (Object.hasOwn(body, field)) throw new LlmError(`DeepSeek request extension field ${JSON.stringify(field)} collides with the base request`, "REQUEST_EXTENSION");
1383
+ let payload;
1384
+ try {
1385
+ payload = JSON.stringify({
1386
+ ...body,
1387
+ ...extensions.fields
1388
+ });
1389
+ } catch (error) {
1390
+ const base = JSON.stringify(body);
1391
+ onOmitted(fields, error);
1392
+ return {
1393
+ payload: base,
1394
+ accept: () => Promise.resolve()
1395
+ };
1396
+ }
1397
+ return {
1398
+ payload,
1399
+ async accept() {
1400
+ try {
1401
+ await extensions.accept();
1402
+ } catch (error) {
1403
+ onUnaccepted(error);
1404
+ }
1405
+ }
1406
+ };
1407
+ }
1408
+ //#endregion
1409
+ //#region lib/types/images.js
1410
+ /** Deterministic Messages image preparation for Files references and bounded inline fallback. */
1411
+ function bounds(connection, representation) {
1412
+ return {
1413
+ representation,
1414
+ maxBytes: representation === "raw" ? connection.maxRequestFilesBytes : connection.maxInlineRequestImageBytes,
1415
+ maxImages: connection.maxImagesPerRequest,
1416
+ byteQuantum: representation === "raw" ? connection.imageOffloadByteQuantum : connection.inlineImageOffloadByteQuantum,
1417
+ countQuantum: connection.imageOffloadCountQuantum
1418
+ };
1419
+ }
1420
+ function* imageRefs(blocks) {
1421
+ for (const block of blocks) if (block.type === "image") yield block.attachment;
1422
+ }
1423
+ /** Normalize retained image references before converting Messages content.
1424
+ * @param history - durable history; never mutated.
1425
+ * @param connection - request-local image budgets.
1426
+ * @param modelId - target model id.
1427
+ * @param attachments - mounted attachment store, required only for image requests.
1428
+ * @param access - current execution-world path resolver.
1429
+ * @param signal - request cancellation.
1430
+ * @returns projected history and prepared image bytes keyed by attachment id.
1431
+ */
1432
+ async function prepareImages(history, connection, modelId, attachments, access, signal) {
1433
+ const versions = /* @__PURE__ */ new Map();
1434
+ const messages = projectOffloadedImages(history, (ref) => offloadedImageText(ref, access(ref)));
1435
+ if (!messages.some((message) => contentHasImage(message.content))) return {
1436
+ messages,
1437
+ versions
1438
+ };
1439
+ const model = connection.models.find((entry) => entry.id === modelId);
1440
+ if (model?.inputModalities?.includes("image") !== true || attachments === void 0) throw new LlmError("DeepSeek Messages image input requires a vision model and attachment service", "UNSUPPORTED_CONTENT");
1441
+ if (messages.some((message) => message.role !== "user" && message.role !== "tool" && contentHasImage(message.content))) throw new LlmError("DeepSeek Messages supports images only in user messages and tool results", "UNSUPPORTED_CONTENT");
1442
+ for (const message of messages) for (const ref of imageRefs(message.content)) if (!versions.has(ref.attachmentId)) versions.set(ref.attachmentId, await attachments.readImageRequest(ref, resolveRequestImageTarget(model, ref), signal));
1443
+ assertImagesFit(messages, versions, connection, "raw");
1444
+ return {
1445
+ messages,
1446
+ versions
1447
+ };
1448
+ }
1449
+ /** Require logged offload before retrying images that exceed the inline budget.
1450
+ * @param messages - history already within the Files budget.
1451
+ * @param versions - normalized versions prepared for retained references.
1452
+ * @param connection - resolved inline bounds.
1453
+ * @returns unchanged history within both byte and image-count limits.
1454
+ */
1455
+ function inlineImages(messages, versions, connection) {
1456
+ assertImagesFit(messages, versions, connection, "base64");
1457
+ return messages;
1458
+ }
1459
+ /** Count additional oldest occurrences requiring durable offload at their exact represented bytes. */
1460
+ function assertImagesFit(messages, versions, connection, representation) {
1461
+ const offloadImages = requiredImageOffload(messages, bounds(connection, representation), (block) => versions.get(block.attachment.attachmentId).bytes);
1462
+ if (offloadImages > 0) throw new LlmError(`DeepSeek Messages ${representation} request images exceed the route budget; ${offloadImages} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages });
1463
+ }
1464
+ /** Resolve retained images to Files ids, recording every occurrence for failure diagnostics.
1465
+ * @param messages - history within the Files byte/count budget.
1466
+ * @param versions - normalized versions for every retained reference.
1467
+ * @param files - request-owned Files resolution and recovery.
1468
+ * @returns ids keyed by durable attachment identity.
1469
+ */
1470
+ async function prepareFileIds(messages, versions, files) {
1471
+ const ids = /* @__PURE__ */ new Map();
1472
+ for (const [index, message] of messages.entries()) {
1473
+ let image = 0;
1474
+ for (const ref of imageRefs(message.content)) {
1475
+ const version = versions.get(ref.attachmentId);
1476
+ ids.set(ref.attachmentId, await files.resolve(version, {
1477
+ message: index + 1,
1478
+ image: ++image
1479
+ }));
1480
+ }
1481
+ }
1482
+ return ids;
1483
+ }
1484
+ //#endregion
1485
+ //#region lib/types/replay.js
1486
+ /** Minimal native thinking metadata; durable Harness blocks own all response text. */
1487
+ /** Reject malformed JSON objects at provider and durable-data reads.
1488
+ * @param value - untrusted decoded JSON.
1489
+ * @param code - owning failure category.
1490
+ * @returns the validated object.
1491
+ */
1492
+ function object(value, code = "MALFORMED_RESPONSE") {
1493
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new LlmError("DeepSeek Messages expected a JSON object", code);
1494
+ return value;
1495
+ }
1496
+ /** Construct response metadata without duplicating the assistant text.
1497
+ * @param model - requested model identity.
1498
+ * @param blocks - metadata in emitted block order.
1499
+ * @returns the versioned envelope persisted by the existing assembler.
1500
+ */
1501
+ function replayState(model, blocks) {
1502
+ return {
1503
+ response: {
1504
+ kind: "deepseek-messages",
1505
+ version: 1,
1506
+ model
1507
+ },
1508
+ blocks
1509
+ };
1510
+ }
1511
+ /** Validate native replay, discarding unusable metadata before serializing durable content.
1512
+ * @param message - durable assistant content and source metadata.
1513
+ * @param model - target model; cross-model signatures are not portable.
1514
+ * @param onDegrade - diagnostic for unusable metadata; receives no message content or signatures.
1515
+ * @returns index-aligned metadata, absent for foreign, cross-model or degraded history.
1516
+ */
1517
+ function readReplay(message, model, onDegrade) {
1518
+ try {
1519
+ return validateReplay(message, model);
1520
+ } catch (error) {
1521
+ /* v8 ignore next -- the validator only throws INVALID_REPLAY_STATE; preserve future non-replay failures. */
1522
+ if (!(error instanceof LlmError) || error.code !== "INVALID_REPLAY_STATE") throw error;
1523
+ onDegrade?.(error.message);
1524
+ return;
1525
+ }
1526
+ }
1527
+ function validateReplay(message, model) {
1528
+ if (message.source.kind !== "model" || message.source.replayState === void 0) return void 0;
1529
+ const fail = (detail) => {
1530
+ throw new LlmError(`DeepSeek Messages replay: ${detail}`, "INVALID_REPLAY_STATE");
1531
+ };
1532
+ const envelope = object(message.source.replayState, "INVALID_REPLAY_STATE");
1533
+ const response = object(envelope.response, "INVALID_REPLAY_STATE");
1534
+ if (response.kind !== "deepseek-messages" || response.version !== 1) return fail("unsupported kind or version");
1535
+ if (response.model !== message.source.model) return fail("model does not match assistant source model");
1536
+ if (!Array.isArray(envelope.blocks) || envelope.blocks.length !== message.content.length) return fail("block count mismatch");
1537
+ const blocks = envelope.blocks.map((value, index) => {
1538
+ const block = object(value, "INVALID_REPLAY_STATE");
1539
+ if (block.type !== message.content[index]?.type || block.type !== "text" && block.type !== "reasoning" && block.type !== "tool-call") return fail("block type mismatch");
1540
+ if (block.signature !== void 0 && (block.type !== "reasoning" || typeof block.signature !== "string")) return fail("invalid signature");
1541
+ return {
1542
+ type: block.type,
1543
+ ...typeof block.signature === "string" ? { signature: block.signature } : {}
1544
+ };
1545
+ });
1546
+ return response.model === model ? blocks : void 0;
1547
+ }
1548
+ //#endregion
1549
+ //#region lib/types/serialize.js
1550
+ /** Map system snapshots, tool changes, and conversation turns to Messages using the configured route capability. */
1551
+ function unsupported(type) {
1552
+ throw new LlmError(`DeepSeek Messages cannot represent ${type}`, "UNSUPPORTED_CONTENT");
1553
+ }
1554
+ /** Historical arguments that Messages cannot represent use empty input; durable content stays unchanged. */
1555
+ function toolInput(raw) {
1556
+ let value;
1557
+ try {
1558
+ value = JSON.parse(raw);
1559
+ } catch (_invalidToolHistoryJson) {
1560
+ return {};
1561
+ }
1562
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1563
+ }
1564
+ function assistant(message, model, onReplayDegrade) {
1565
+ const replay = readReplay(message, model, onReplayDegrade);
1566
+ return message.content.flatMap((block, index) => {
1567
+ switch (block.type) {
1568
+ case "text": return block.text.length === 0 ? [] : {
1569
+ type: "text",
1570
+ text: block.text
1571
+ };
1572
+ case "reasoning": return {
1573
+ type: "thinking",
1574
+ thinking: block.text,
1575
+ ...replay?.[index]?.signature === void 0 ? {} : { signature: replay[index].signature }
1576
+ };
1577
+ case "tool-call": return {
1578
+ type: "tool_use",
1579
+ id: block.id,
1580
+ name: block.name,
1581
+ input: toolInput(block.arguments)
1582
+ };
1583
+ default: return unsupported(`assistant content ${block.type}`);
1584
+ }
1585
+ });
1586
+ }
1587
+ /** Serialize one complete request using already prepared image bytes.
1588
+ * User and tool-result content omits reasoning and tool-call blocks.
1589
+ * Empty assistant text blocks and empty user/assistant messages are skipped;
1590
+ * empty tool results retain their call ids.
1591
+ * Developer messages, already projected for this route by `LlmRuntime`, become
1592
+ * system-role updates after the preceding user turn, with tool changes as
1593
+ * `tool_addition` and `tool_removal` references to the declared tool.
1594
+ * @param options - provider-neutral request.
1595
+ * @param connection - validated defaults and thinking policy.
1596
+ * @param history - image-projected history with complete system snapshots; durable messages remain unchanged.
1597
+ * @param images - request versions for retained images.
1598
+ * @param access - execution-world paths for image descriptions.
1599
+ * @param onReplayDegrade - diagnostic for discarded native replay metadata.
1600
+ * @param fileIds - resolved Files references; omission selects inline image bytes.
1601
+ * @returns the Messages API JSON body.
1602
+ */
1603
+ function serialize(options, connection, history, images, access, onReplayDegrade, fileIds) {
1604
+ const model = connection.models.find((entry) => entry.id === options.model);
1605
+ const inHistory = model?.systemPromptUpdate === "in-history";
1606
+ const input = (blocks) => blocks.flatMap((block) => {
1607
+ if (block.type === "text") return block.text ? [{
1608
+ type: "text",
1609
+ text: block.text
1610
+ }] : [];
1611
+ if (block.type === "reasoning" || block.type === "tool-call") return [];
1612
+ if (block.type !== "image") return unsupported(`user/tool-result content ${block.type}`);
1613
+ const version = images.get(block.attachment.attachmentId);
1614
+ if (version === void 0) throw new LlmError("DeepSeek Messages request image is missing", "INVALID_REQUEST");
1615
+ const fileId = fileIds?.get(block.attachment.attachmentId);
1616
+ if (fileIds !== void 0 && fileId === void 0) throw new LlmError("DeepSeek Messages request file id is missing", "INVALID_REQUEST");
1617
+ return [{
1618
+ type: "text",
1619
+ text: requestImageHandleText(block.attachment, version, access(block.attachment))
1620
+ }, fileId === void 0 ? {
1621
+ type: "image",
1622
+ source: {
1623
+ type: "base64",
1624
+ media_type: version.mediaType,
1625
+ data: Buffer.from(version.data).toString("base64")
1626
+ }
1627
+ } : {
1628
+ type: "image",
1629
+ source: {
1630
+ type: "file",
1631
+ file_id: fileId
1632
+ }
1633
+ }];
1634
+ });
1635
+ const messages = [];
1636
+ let historySystem;
1637
+ const systemUpdates = [];
1638
+ const flushSystemUpdates = () => {
1639
+ if (systemUpdates.length === 0) return;
1640
+ if (messages.at(-1)?.role !== "user") return unsupported("system update without a preceding user or tool-result turn");
1641
+ messages.push(...systemUpdates.splice(0));
1642
+ };
1643
+ for (const message of history) {
1644
+ if (message.role === "developer") {
1645
+ const content = message.content.flatMap((block) => {
1646
+ switch (block.type) {
1647
+ case "text": return block.text.length === 0 ? [] : [{
1648
+ type: "text",
1649
+ text: block.text
1650
+ }];
1651
+ case "tool-addition": return [{
1652
+ type: "tool_addition",
1653
+ tool: {
1654
+ type: "tool_reference",
1655
+ name: block.toolName
1656
+ }
1657
+ }];
1658
+ case "tool-removal": return [{
1659
+ type: "tool_removal",
1660
+ tool: {
1661
+ type: "tool_reference",
1662
+ name: block.toolName
1663
+ }
1664
+ }];
1665
+ default: return unsupported(`developer content ${block.type}`);
1666
+ }
1667
+ });
1668
+ if (content.length > 0) systemUpdates.push({
1669
+ role: "system",
1670
+ content
1671
+ });
1672
+ continue;
1673
+ }
1674
+ if (message.content.some((block) => block.type === "tool-addition" || block.type === "tool-removal")) return unsupported("tool-change blocks outside developer messages");
1675
+ if (message.role === "system") {
1676
+ const texts = message.content.filter((block) => block.type === "text");
1677
+ if (texts.length !== message.content.length) return unsupported("non-text system message");
1678
+ const text = texts.map((block) => block.text).join("");
1679
+ if (inHistory && messages.length > 0) {
1680
+ if (text.length === 0) return unsupported("empty in-history system update");
1681
+ systemUpdates.push({
1682
+ role: "system",
1683
+ content: [{
1684
+ type: "text",
1685
+ text
1686
+ }]
1687
+ });
1688
+ } else historySystem = text;
1689
+ continue;
1690
+ }
1691
+ const content = message.role === "assistant" ? assistant(message, options.model, onReplayDegrade) : message.role === "tool" ? [{
1692
+ type: "tool_result",
1693
+ tool_use_id: message.toolCallId,
1694
+ content: input(message.content),
1695
+ ...message.isError === void 0 ? {} : { is_error: message.isError }
1696
+ }] : message.content.flatMap((block) => input([block]));
1697
+ if ((message.role === "user" || message.role === "assistant") && content.length === 0) continue;
1698
+ if (message.role === "assistant") flushSystemUpdates();
1699
+ const wireRole = message.role === "tool" ? "user" : message.role;
1700
+ const previous = messages.at(-1);
1701
+ if (previous?.role === wireRole) previous.content.push(...content);
1702
+ else messages.push({
1703
+ role: wireRole,
1704
+ content
1705
+ });
1706
+ }
1707
+ flushSystemUpdates();
1708
+ let pending = /* @__PURE__ */ new Set();
1709
+ for (const message of messages) if (message.role === "assistant") {
1710
+ const calls = message.content.filter((block) => block.type === "tool_use");
1711
+ pending = new Set(calls.map((block) => block.id));
1712
+ if (pending.size !== calls.length) throw new LlmError("DeepSeek Messages duplicate tool call id", "INVALID_REQUEST");
1713
+ } else if (message.role === "user") {
1714
+ const results = message.content.filter((block) => block.type === "tool_result");
1715
+ for (const result of results) if (!pending.delete(result.tool_use_id)) throw new LlmError("DeepSeek Messages tool result has no matching call", "INVALID_REQUEST");
1716
+ if (pending.size > 0) throw new LlmError("DeepSeek Messages tool calls need immediate results", "INVALID_REQUEST");
1717
+ message.content = [...results, ...message.content.filter((block) => block.type !== "tool_result")];
1718
+ }
1719
+ if (pending.size > 0) throw new LlmError("DeepSeek Messages history ends with unresolved tools", "INVALID_REQUEST");
1720
+ const effort = options.reasoningEffort ?? connection.defaults.reasoningEffort ?? (connection.defaults.thinking === "disabled" ? "off" : "high");
1721
+ if (![
1722
+ "off",
1723
+ "low",
1724
+ "high",
1725
+ "max"
1726
+ ].includes(effort) || connection.defaults.thinking === "disabled" && effort !== "off") throw new LlmError(`DeepSeek Messages does not support reasoning effort ${effort}`, "UNSUPPORTED_REASONING_EFFORT");
1727
+ const system = [options.system, historySystem].filter(Boolean).join("\n\n");
1728
+ return {
1729
+ model: options.model,
1730
+ stream: true,
1731
+ messages,
1732
+ max_tokens: options.maxTokens ?? model?.maxTokens ?? connection.maxTokens,
1733
+ thinking: { type: effort === "off" ? "disabled" : "enabled" },
1734
+ ...effort === "off" ? {} : { output_config: { effort } },
1735
+ ...system.length === 0 ? {} : { system },
1736
+ ...options.temperature === void 0 ? {} : { temperature: options.temperature },
1737
+ ...options.stop === void 0 ? {} : { stop_sequences: options.stop },
1738
+ ...options.tools === void 0 ? {} : { tools: options.tools.map((tool) => ({
1739
+ name: tool.name,
1740
+ description: tool.description,
1741
+ input_schema: tool.parameters,
1742
+ ...tool.deferLoading === true ? { defer_loading: true } : {}
1743
+ })) }
1744
+ };
1745
+ }
1746
+ //#endregion
1747
+ //#region lib/types/transport.js
1748
+ /** Normalize HTTP and in-band Messages errors into provider-neutral failures. */
1749
+ /** Read only provider error fields used by bounded Files recovery.
1750
+ * @param raw - decoded HTTP error response.
1751
+ * @returns code, type, and message text, without unrelated response fields.
1752
+ */
1753
+ function providerErrorDetail(raw) {
1754
+ const error = typeof raw === "object" && raw !== null && "error" in raw ? raw.error : void 0;
1755
+ if (typeof error !== "object" || error === null) return "";
1756
+ const fields = error;
1757
+ return [
1758
+ fields.code,
1759
+ fields.type,
1760
+ fields.message
1761
+ ].filter((value) => typeof value === "string").join(" ");
1762
+ }
1763
+ /** Classify a provider error without trusting arbitrary response fields.
1764
+ * @param raw - decoded response or in-band error event.
1765
+ * @param status - HTTP status when the error preceded streaming.
1766
+ * @param headers - response headers for retry delay and request identity.
1767
+ * @returns a stable error consumed by LlmRuntime and llm-retry.
1768
+ */
1769
+ function providerError(raw, status, headers) {
1770
+ const envelope = typeof raw === "object" && raw !== null ? raw : {};
1771
+ const error = typeof envelope.error === "object" && envelope.error !== null ? envelope.error : {};
1772
+ const message = typeof error.message === "string" ? error.message : `DeepSeek Messages request failed (${status ?? "stream error"})`;
1773
+ const type = typeof error.type === "string" ? error.type : "";
1774
+ const detail = `${type} ${typeof error.code === "string" ? error.code : ""} ${message}`;
1775
+ let code;
1776
+ if (status === 401 || status === 403 || ["authentication_error", "permission_error"].includes(type)) code = "AUTH";
1777
+ else if (isQuotaExceededError(detail) || status === 402) code = "QUOTA";
1778
+ else if (status === 429 || type === "rate_limit_error") code = "RATE_LIMIT";
1779
+ else if (isContextWindowExceededError(detail)) code = "CONTEXT_WINDOW_EXCEEDED";
1780
+ else if (status === 400 || status === 413 || type === "invalid_request_error") code = "INVALID_REQUEST";
1781
+ else if (status !== void 0 && status >= 500 || ["api_error", "overloaded_error"].includes(type)) code = "SERVER";
1782
+ else code = status === void 0 ? "SERVER" : `HTTP_${status}`;
1783
+ const retry = headers?.get("retry-after");
1784
+ const delay = retry == null ? NaN : /^\d+(?:\.\d+)?$/u.test(retry) ? Number(retry) * 1e3 : Date.parse(retry) - Date.now();
1785
+ const id = headers?.get("request-id") ?? headers?.get("x-request-id") ?? headers?.get("x-deepseek-request-id");
1786
+ return new LlmError(message, code, {
1787
+ ...status === void 0 ? {} : { status },
1788
+ ...id ? { requestId: ProviderRequestId(id) } : {},
1789
+ ...Number.isFinite(delay) && delay > 0 ? { providerRetryAfterMs: delay } : {}
1790
+ });
1791
+ }
1792
+ //#endregion
1793
+ //#region lib/types/sse.js
1794
+ /** SSE framing delegated to eventsource-parser; JSON errors remain provider failures. */
1795
+ /** Decode complete SSE frames without treating an unterminated tail as an event.
1796
+ * @param body - provider response bytes.
1797
+ * @param activity - pulse the idle watchdog for events and heartbeat comments.
1798
+ * @returns JSON events, including message_stop; the translator owns completion.
1799
+ */
1800
+ async function* parseSse(body, activity) {
1801
+ const events = body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream({ onComment: activity }));
1802
+ for await (const frame of events) {
1803
+ activity();
1804
+ let raw;
1805
+ try {
1806
+ raw = JSON.parse(frame.data);
1807
+ } catch (_invalidSseJson) {
1808
+ throw new LlmError("DeepSeek Messages SSE contains invalid JSON", "MALFORMED_RESPONSE");
1809
+ }
1810
+ const event = object(raw);
1811
+ if (typeof event.type !== "string" || frame.event !== void 0 && frame.event !== event.type) throw new LlmError("DeepSeek Messages SSE event type mismatch", "MALFORMED_RESPONSE");
1812
+ if (event.type === "error") throw providerError(event, void 0);
1813
+ yield event;
1814
+ }
1815
+ }
1816
+ //#endregion
1817
+ //#region lib/types/translate.js
1818
+ /** Translate Messages events while preserving block order and cumulative usage. */
1819
+ /** Decode a required string from provider JSON.
1820
+ * @param value - provider field.
1821
+ * @returns the validated string.
1822
+ */
1823
+ function string(value) {
1824
+ if (typeof value !== "string") throw new LlmError("DeepSeek Messages expected a string field", "MALFORMED_RESPONSE");
1825
+ return value;
1826
+ }
1827
+ function malformed(detail) {
1828
+ throw new LlmError(`DeepSeek Messages stream: ${detail}`, "MALFORMED_RESPONSE");
1829
+ }
1830
+ function indexOf(event) {
1831
+ if (!Number.isSafeInteger(event.index) || event.index < 0) return malformed("invalid block index");
1832
+ return event.index;
1833
+ }
1834
+ function updateUsage(usage, raw) {
1835
+ const fields = object(raw);
1836
+ for (const [wire, local] of Object.entries({
1837
+ input_tokens: "inputTokens",
1838
+ output_tokens: "outputTokens",
1839
+ cache_read_input_tokens: "cacheReadTokens",
1840
+ cache_creation_input_tokens: "cacheWriteTokens"
1841
+ })) {
1842
+ const value = fields[wire];
1843
+ if (value === void 0) continue;
1844
+ if (!Number.isSafeInteger(value) || value < 0) return malformed(`invalid ${wire}`);
1845
+ usage[local] = value;
1846
+ }
1847
+ }
1848
+ function startBlock(event, index) {
1849
+ const native = object(event.content_block);
1850
+ let content;
1851
+ let replay;
1852
+ switch (native.type) {
1853
+ case "text":
1854
+ content = {
1855
+ type: "text",
1856
+ text: string(native.text)
1857
+ };
1858
+ replay = { type: "text" };
1859
+ break;
1860
+ case "thinking":
1861
+ content = {
1862
+ type: "reasoning",
1863
+ text: string(native.thinking)
1864
+ };
1865
+ replay = {
1866
+ type: "reasoning",
1867
+ ...native.signature === void 0 ? {} : { signature: string(native.signature) }
1868
+ };
1869
+ break;
1870
+ case "tool_use":
1871
+ content = {
1872
+ type: "tool-call",
1873
+ id: ToolCallId(string(native.id)),
1874
+ name: string(native.name),
1875
+ arguments: JSON.stringify(object(native.input))
1876
+ };
1877
+ if (!content.id || !content.name) return malformed("empty tool identity");
1878
+ replay = { type: "tool-call" };
1879
+ break;
1880
+ default: throw new LlmError(`DeepSeek Messages does not support response block ${String(native.type)}`, "UNSUPPORTED_CONTENT");
1881
+ }
1882
+ return {
1883
+ index,
1884
+ content,
1885
+ replay,
1886
+ closed: false,
1887
+ json: ""
1888
+ };
1889
+ }
1890
+ function deltaChunk(block, raw) {
1891
+ const delta = object(raw);
1892
+ const content = block.content;
1893
+ if (delta.type === "text_delta" && content.type === "text") {
1894
+ const text = string(delta.text);
1895
+ content.text += text;
1896
+ return {
1897
+ type: "text-delta",
1898
+ index: block.index,
1899
+ text
1900
+ };
1901
+ }
1902
+ if (delta.type === "thinking_delta" && content.type === "reasoning") {
1903
+ const text = string(delta.thinking);
1904
+ content.text += text;
1905
+ return {
1906
+ type: "reasoning-delta",
1907
+ index: block.index,
1908
+ text
1909
+ };
1910
+ }
1911
+ if (delta.type === "signature_delta" && content.type === "reasoning") {
1912
+ block.replay.signature = (block.replay.signature ?? "") + string(delta.signature);
1913
+ return;
1914
+ }
1915
+ if (delta.type === "input_json_delta" && content.type === "tool-call") {
1916
+ const argumentsDelta = string(delta.partial_json);
1917
+ block.json += argumentsDelta;
1918
+ return {
1919
+ type: "tool-call-delta",
1920
+ index: block.index,
1921
+ id: content.id,
1922
+ argumentsDelta
1923
+ };
1924
+ }
1925
+ return malformed(`unsupported delta ${String(delta.type)} for ${content.type}`);
1926
+ }
1927
+ function stopReason(raw) {
1928
+ switch (raw) {
1929
+ case "end_turn":
1930
+ case "stop_sequence": return { kind: "stop" };
1931
+ case "tool_use": return { kind: "tool-calls" };
1932
+ case "max_tokens": return { kind: "max-tokens" };
1933
+ default: return malformed(`unsupported stop reason ${String(raw)}`);
1934
+ }
1935
+ }
1936
+ /** Translate decoded SSE data into the Harness stream protocol.
1937
+ * @param events - framed, decoded provider events in arrival order.
1938
+ * @param model - requested model id stored in durable replay state.
1939
+ * @returns blocks, one final usage value, and exactly one terminal finish.
1940
+ */
1941
+ async function* translate(events, model) {
1942
+ const blocks = /* @__PURE__ */ new Map();
1943
+ const usage = {
1944
+ inputTokens: 0,
1945
+ outputTokens: 0
1946
+ };
1947
+ let started = false;
1948
+ let reason;
1949
+ for await (const event of events) {
1950
+ if (event.type === "message_start") {
1951
+ if (started) return malformed("duplicate message_start");
1952
+ updateUsage(usage, object(event.message).usage);
1953
+ started = true;
1954
+ continue;
1955
+ }
1956
+ if (![
1957
+ "content_block_start",
1958
+ "content_block_delta",
1959
+ "content_block_stop",
1960
+ "message_delta",
1961
+ "message_stop"
1962
+ ].includes(String(event.type))) continue;
1963
+ if (!started) return malformed("event precedes message_start");
1964
+ if (event.type === "content_block_start") {
1965
+ const wireIndex = indexOf(event);
1966
+ if (blocks.has(wireIndex) || reason !== void 0) return malformed("block starts after settlement or repeats an index");
1967
+ const block = startBlock(event, blocks.size);
1968
+ blocks.set(wireIndex, block);
1969
+ yield {
1970
+ type: "block-start",
1971
+ index: block.index,
1972
+ blockType: block.content.type
1973
+ };
1974
+ if (block.content.type === "text" || block.content.type === "reasoning") {
1975
+ if (block.content.text) yield {
1976
+ type: block.content.type === "text" ? "text-delta" : "reasoning-delta",
1977
+ index: block.index,
1978
+ text: block.content.text
1979
+ };
1980
+ } else yield {
1981
+ type: "tool-call-delta",
1982
+ index: block.index,
1983
+ id: block.content.id,
1984
+ name: block.content.name,
1985
+ argumentsDelta: ""
1986
+ };
1987
+ } else if (event.type === "content_block_delta" || event.type === "content_block_stop") {
1988
+ const block = blocks.get(indexOf(event));
1989
+ if (block === void 0 || block.closed) return malformed("delta/stop without an open block");
1990
+ if (event.type === "content_block_delta") {
1991
+ const chunk = deltaChunk(block, event.delta);
1992
+ if (chunk !== void 0) yield chunk;
1993
+ } else {
1994
+ block.closed = true;
1995
+ if (block.content.type === "tool-call" && block.json.length > 0) block.content.arguments = block.json;
1996
+ yield {
1997
+ type: "block-end",
1998
+ index: block.index,
1999
+ block: { ...block.content }
2000
+ };
2001
+ }
2002
+ } else if (event.type === "message_delta") {
2003
+ const delta = object(event.delta);
2004
+ if (delta.stop_reason != null) reason = stopReason(delta.stop_reason);
2005
+ if (event.usage !== void 0) updateUsage(usage, event.usage);
2006
+ } else {
2007
+ if (reason === void 0 || [...blocks.values()].some((block) => !block.closed)) return malformed("message_stop without settled blocks and stop reason");
2008
+ if (blocks.size === 0 && reason.kind === "stop") throw new LlmError("DeepSeek Messages returned no content", "EMPTY_RESPONSE");
2009
+ if (reason.kind !== "max-tokens") for (const { content } of blocks.values()) {
2010
+ if (content.type !== "tool-call") continue;
2011
+ let parsed;
2012
+ try {
2013
+ parsed = JSON.parse(content.arguments);
2014
+ } catch (_invalidProviderToolJson) {
2015
+ return malformed("tool input is invalid JSON");
2016
+ }
2017
+ object(parsed);
2018
+ }
2019
+ usage.totalTokens = usage.inputTokens + usage.outputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
2020
+ yield {
2021
+ type: "usage",
2022
+ usage
2023
+ };
2024
+ yield {
2025
+ type: "finish",
2026
+ reason,
2027
+ replayState: replayState(model, [...blocks.values()].map((block) => block.replay))
2028
+ };
2029
+ return;
2030
+ }
2031
+ }
2032
+ throw new LlmError("DeepSeek Messages stream ended before message_stop", "STREAM_CLOSED");
2033
+ }
2034
+ //#endregion
2035
+ //#region lib/types/adapter.js
2036
+ /** Direct Messages transport with one cancellable lifecycle per model request. */
2037
+ var __addDisposableResource = function(env, value, async) {
2038
+ if (value !== null && value !== void 0) {
2039
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
2040
+ var dispose, inner;
2041
+ if (async) {
2042
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
2043
+ dispose = value[Symbol.asyncDispose];
2044
+ }
2045
+ if (dispose === void 0) {
2046
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
2047
+ dispose = value[Symbol.dispose];
2048
+ if (async) inner = dispose;
2049
+ }
2050
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
2051
+ if (inner) dispose = function() {
2052
+ try {
2053
+ inner.call(this);
2054
+ } catch (e) {
2055
+ return Promise.reject(e);
2056
+ }
2057
+ };
2058
+ env.stack.push({
2059
+ value,
2060
+ dispose,
2061
+ async
2062
+ });
2063
+ } else if (async) env.stack.push({ async: true });
2064
+ return value;
2065
+ };
2066
+ var __disposeResources = (function(SuppressedError) {
2067
+ return function(env) {
2068
+ function fail(e) {
2069
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
2070
+ env.hasError = true;
2071
+ }
2072
+ var r, s = 0;
2073
+ function next() {
2074
+ while (r = env.stack.pop()) try {
2075
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
2076
+ if (r.dispose) {
2077
+ var result = r.dispose.call(r.value);
2078
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
2079
+ fail(e);
2080
+ return next();
2081
+ });
2082
+ } else s |= 1;
2083
+ } catch (e) {
2084
+ fail(e);
2085
+ }
2086
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
2087
+ if (env.hasError) throw env.error;
2088
+ }
2089
+ return next();
2090
+ };
2091
+ })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
2092
+ var e = new Error(message);
2093
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
2094
+ });
2095
+ /** DeepSeek provider using Messages content and native thinking replay. */
2096
+ var DeepSeekAdapter = class extends LlmAdapter {
2097
+ dependencies;
2098
+ files;
2099
+ imageAccess = (ref) => {
2100
+ const attachments = this.dependencies.resolveAttachments?.();
2101
+ return attachments === void 0 ? void 0 : this.dependencies.resolveImageAccess?.(attachments, ref);
2102
+ };
2103
+ constructor(dependencies) {
2104
+ super();
2105
+ this.dependencies = dependencies;
2106
+ this.files = dependencies.resolveFiles?.() ?? new DeepSeekFileStore();
2107
+ }
2108
+ providerInfo(provider) {
2109
+ return {
2110
+ id: provider,
2111
+ name: this.dependencies.providerName ?? "DeepSeek"
2112
+ };
2113
+ }
2114
+ providerRetryPolicy(_provider) {
2115
+ return this.dependencies.options().retryPolicy;
2116
+ }
2117
+ async listModels(provider) {
2118
+ return this.dependencies.discoverModels?.(provider) ?? [];
2119
+ }
2120
+ async resolveModel(provider, model, signal) {
2121
+ return modelInfo(await this.connection(signal), provider, model);
2122
+ }
2123
+ imageRequestPricing(_provider, model) {
2124
+ return deepSeekImageRequestPricing(this.dependencies.options(), model, this.imageAccess);
2125
+ }
2126
+ imageRequestTarget(_provider, model, image) {
2127
+ const entry = this.dependencies.options().models.find((entry) => entry.id === model);
2128
+ if (entry === void 0) throw new LlmError(`DeepSeek model "${model}" has no declared image projection`, "UNKNOWN_MODEL");
2129
+ return resolveRequestImageTarget(entry, image);
2130
+ }
2131
+ async prepareCall(provider, model, signal) {
2132
+ const connection = await this.connection(signal);
2133
+ this.dependencies.admit?.(connection, model);
2134
+ return {
2135
+ model: modelInfo(connection, provider, model),
2136
+ stream: (options) => this.generate(options, connection)
2137
+ };
2138
+ }
2139
+ stream(options) {
2140
+ const connection = this.dependencies.options();
2141
+ this.dependencies.admit?.(connection, options.model);
2142
+ return this.generate(options, connection);
2143
+ }
2144
+ connection(signal) {
2145
+ return this.dependencies.connect?.(signal) ?? Promise.resolve(this.dependencies.options());
2146
+ }
2147
+ async *generate(options, connection) {
2148
+ const env_1 = {
2149
+ stack: [],
2150
+ error: void 0,
2151
+ hasError: false
2152
+ };
2153
+ try {
2154
+ const consumer = new AbortController();
2155
+ const watchdog = __addDisposableResource(env_1, idleWatchdog(options.signal === void 0 ? consumer.signal : AbortSignal.any([consumer.signal, options.signal]), connection.streamIdleTimeoutMs, "MESSAGES_IDLE"), false);
2156
+ const iterator = this.request(options, connection, watchdog.signal, () => {
2157
+ watchdog.pulse();
2158
+ });
2159
+ try {
2160
+ while (true) {
2161
+ const next = await watchdog.next(iterator);
2162
+ if (next.done) return;
2163
+ yield next.value;
2164
+ }
2165
+ } catch (error) {
2166
+ if (timeoutOf(watchdog.signal, "MESSAGES_IDLE") !== void 0) throw new LlmError("DeepSeek Messages stream idle timeout", "TIMEOUT", { cause: error });
2167
+ if (options.signal?.aborted) throw new LlmError("DeepSeek Messages request aborted", "ABORTED", { cause: error });
2168
+ if (error instanceof LlmError) throw error;
2169
+ throw new LlmError("DeepSeek Messages transport failed", "TRANSPORT", { cause: error });
2170
+ } finally {
2171
+ consumer.abort();
2172
+ /* v8 ignore next -- the detached drain only rejects while unwinding an abandoned request. */
2173
+ iterator.return(void 0).catch(() => {});
2174
+ }
2175
+ } catch (e_1) {
2176
+ env_1.error = e_1;
2177
+ env_1.hasError = true;
2178
+ } finally {
2179
+ __disposeResources(env_1);
2180
+ }
2181
+ }
2182
+ async *request(options, connection, signal, activity) {
2183
+ signal.throwIfAborted();
2184
+ const transport = this.dependencies.createTransport === void 0 ? { send: (url, init) => fetch(url, init) } : this.dependencies.createTransport(options);
2185
+ if (transport === null || typeof transport !== "object" || typeof transport.send !== "function") throw new TypeError("DeepSeek createTransport must return an object with send(url, init)");
2186
+ const { messages, versions } = await prepareImages(options.messages, connection, options.model, this.dependencies.resolveAttachments?.(), this.imageAccess, signal);
2187
+ const auth = await this.dependencies.resolveAuth(connection);
2188
+ try {
2189
+ const files = new RequestFiles(this.files, {
2190
+ baseURL: connection.baseURL,
2191
+ headers: auth.headers
2192
+ }, connection.filePolicy, connection.filesApiTimeoutMs, signal, activity);
2193
+ let inline = false;
2194
+ while (true) {
2195
+ signal.throwIfAborted();
2196
+ files.beginAttempt();
2197
+ let fileIds;
2198
+ if (!inline) try {
2199
+ fileIds = await prepareFileIds(messages, versions, files);
2200
+ } catch (error) {
2201
+ if (!(error instanceof FileResolutionFailure)) throw error;
2202
+ inline = true;
2203
+ continue;
2204
+ }
2205
+ const body = serialize(options, connection, inline ? inlineImages(messages, versions, connection) : messages, versions, this.imageAccess, (reason) => {
2206
+ this.dependencies.onReplayDegrade?.({
2207
+ provider: options.provider,
2208
+ model: options.model,
2209
+ reason
2210
+ });
2211
+ }, fileIds);
2212
+ const requestIdentity = {
2213
+ signal,
2214
+ ...options.sessionId === void 0 ? {} : { sessionId: String(options.sessionId) },
2215
+ ...options.purpose === void 0 ? {} : { purpose: options.purpose }
2216
+ };
2217
+ const extensions = await prepareRequestExtensions(body, requestIdentity, this.dependencies.prepareExtensions, (fields, error) => {
2218
+ this.dependencies.onExtensionsOmitted?.({
2219
+ provider: options.provider,
2220
+ model: options.model,
2221
+ fields,
2222
+ error
2223
+ });
2224
+ }, (error) => {
2225
+ this.dependencies.onExtensionsUnaccepted?.({
2226
+ provider: options.provider,
2227
+ model: options.model,
2228
+ error
2229
+ });
2230
+ });
2231
+ signal.throwIfAborted();
2232
+ const betas = [...fileIds !== void 0 && fileIds.size > 0 ? [MESSAGES_FILES_BETA] : [], ...body.messages.some((message) => message.content.some((block) => block.type === "tool_addition" || block.type === "tool_removal")) ? [MESSAGES_TOOL_CHANGES_BETA] : []];
2233
+ const response = await transport.send(`${messagesApiRoot(connection.baseURL)}/messages`, {
2234
+ method: "POST",
2235
+ signal,
2236
+ body: extensions.payload,
2237
+ redirect: "error",
2238
+ headers: {
2239
+ ...attributionHeaders(),
2240
+ "content-type": "application/json",
2241
+ "accept": "text/event-stream",
2242
+ ...auth.headers,
2243
+ "anthropic-version": "2023-06-01",
2244
+ ...betas.length === 0 ? {} : { "anthropic-beta": betas.join(",") },
2245
+ "x-deepseek-harness-user-id": this.dependencies.resolveUserId(),
2246
+ ...options.sessionId === void 0 ? {} : { "x-deepseek-harness-session-id": String(options.sessionId) },
2247
+ ...requestIdentity.purpose === "compaction" ? { "x-deepseek-harness-compact": "1" } : {}
2248
+ }
2249
+ });
2250
+ if (!response.ok) {
2251
+ const text = await response.text();
2252
+ let raw;
2253
+ try {
2254
+ raw = JSON.parse(text);
2255
+ } catch (_nonJsonGatewayError) {}
2256
+ const detail = providerErrorDetail(raw);
2257
+ if (await files.retry(detail)) continue;
2258
+ const failure = providerError(raw, response.status, response.headers);
2259
+ throw new LlmError(files.errorMessage(response.status, failure.message, detail), failure.code, {
2260
+ ...failure.failure,
2261
+ cause: new Error(text)
2262
+ });
2263
+ }
2264
+ await extensions.accept();
2265
+ if (response.body === null) throw new LlmError("DeepSeek Messages returned no response body", "EMPTY_RESPONSE");
2266
+ yield* translate(parseSse(response.body, activity), options.model);
2267
+ return;
2268
+ }
2269
+ } catch (error) {
2270
+ if (auth.onRequestError !== void 0) {
2271
+ let mapped;
2272
+ try {
2273
+ mapped = await auth.onRequestError(error);
2274
+ } catch (_credentialUpdateFailed) {
2275
+ throw error;
2276
+ }
2277
+ throw mapped;
2278
+ }
2279
+ throw error;
2280
+ }
2281
+ }
2282
+ };
2283
+ //#endregion
2284
+ //#region lib/types/listing.js
2285
+ /** DeepSeek `GET /models` listing: one authenticated request and per-entry decoding. */
2286
+ const LISTED_MODALITIES = ["text", "image"];
2287
+ const BADGE_TONES = [
2288
+ "neutral",
2289
+ "info",
2290
+ "success",
2291
+ "warning"
2292
+ ];
2293
+ const LANGUAGE_ID = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/u;
2294
+ const UNREADABLE_BADGE_CHARACTER = /[\p{Cc}\p{Zl}\p{Zp}\p{Bidi_Control}]/u;
2295
+ const VISIBLE_BADGE_CHARACTER = /[^\p{White_Space}\p{Default_Ignorable_Code_Point}]/u;
2296
+ const GRAPHEMES = new Intl.Segmenter(void 0, { granularity: "grapheme" });
2297
+ /** Code of a listing deadline's timeout reason; {@link requestModelListing} reports it as `TIMEOUT`. */
2298
+ const LISTING_TIMEOUT = "MODEL_LISTING_TIMEOUT";
2299
+ /**
2300
+ * The listing endpoint of a Messages root.
2301
+ * @param baseURL - Messages `baseURL`.
2302
+ * @returns `/models` at the origin of `baseURL`, the only origin that receives the route's credential.
2303
+ */
2304
+ function modelListingURL(baseURL) {
2305
+ return new URL("/models", new URL(baseURL).origin).href;
2306
+ }
2307
+ function fieldsOf(value) {
2308
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2309
+ }
2310
+ /** Listed modalities this build sends, in listing order without repeats; `undefined` when the value is not an array. */
2311
+ function knownModalities(value) {
2312
+ if (!Array.isArray(value)) return void 0;
2313
+ const listed = value;
2314
+ return LISTED_MODALITIES.filter((modality) => listed.includes(modality)).toSorted((left, right) => listed.indexOf(left) - listed.indexOf(right));
2315
+ }
2316
+ /** A declared capacity: `undefined` when absent, `false` when not a positive safe integer. */
2317
+ function capacity(value) {
2318
+ if (value === void 0) return void 0;
2319
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : false;
2320
+ }
2321
+ function requirementOf(entry) {
2322
+ if (entry.client_requirements === void 0) return { kind: "none" };
2323
+ const requirements = fieldsOf(entry.client_requirements);
2324
+ if (requirements === void 0) return { kind: "invalid" };
2325
+ const declared = requirements[APP_IDENTITY.product];
2326
+ if (declared === void 0) return { kind: "none" };
2327
+ const own = fieldsOf(declared);
2328
+ if (own === void 0) return { kind: "invalid" };
2329
+ if (own.min_version === void 0) return { kind: "none" };
2330
+ return typeof own.min_version === "string" && own.min_version.length > 0 ? {
2331
+ kind: "min-version",
2332
+ version: own.min_version
2333
+ } : { kind: "invalid" };
2334
+ }
2335
+ /** One trimmed, visible badge text on one line, without control characters and at most {@link MAX_BADGE_TEXT_LENGTH} graphemes. */
2336
+ function badgeText(value) {
2337
+ if (typeof value !== "string") return void 0;
2338
+ const text = value.trim();
2339
+ return Array.from(GRAPHEMES.segment(text)).length <= 12 && VISIBLE_BADGE_CHARACTER.test(text) && !UNREADABLE_BADGE_CHARACTER.test(value) ? text : void 0;
2340
+ }
2341
+ /** A literal badge text, or translations keyed by lowercase language id that include English. */
2342
+ function localizedBadgeText(value) {
2343
+ if (typeof value === "string") return badgeText(value);
2344
+ const variants = fieldsOf(value);
2345
+ if (variants === void 0) return void 0;
2346
+ const texts = [];
2347
+ for (const [language, variant] of Object.entries(variants)) {
2348
+ const text = badgeText(variant);
2349
+ if (!LANGUAGE_ID.test(language) || text === void 0) return void 0;
2350
+ texts.push([language, text]);
2351
+ }
2352
+ const translations = Object.fromEntries(texts);
2353
+ const en = translations.en;
2354
+ return en === void 0 ? void 0 : {
2355
+ ...translations,
2356
+ en
2357
+ };
2358
+ }
2359
+ /** This client's entry in a listing entry's `client_metadata`, as diagnostics name it. */
2360
+ const OWN_METADATA = `client_metadata["${APP_IDENTITY.product}"]`;
2361
+ /** An optional object field: `undefined` when absent or `null`, `false` when it is not an object. */
2362
+ function optionalFields(value) {
2363
+ return value === void 0 || value === null ? void 0 : fieldsOf(value) ?? false;
2364
+ }
2365
+ function badgeOf(entry) {
2366
+ const metadata = optionalFields(entry.client_metadata);
2367
+ if (metadata === false) return {
2368
+ kind: "invalid",
2369
+ reason: "client_metadata is not an object"
2370
+ };
2371
+ const own = optionalFields(metadata?.[APP_IDENTITY.product]);
2372
+ if (own === false) return {
2373
+ kind: "invalid",
2374
+ reason: `${OWN_METADATA} is not an object`
2375
+ };
2376
+ const declared = optionalFields(own?.badge);
2377
+ if (declared === false) return {
2378
+ kind: "invalid",
2379
+ reason: `${OWN_METADATA}.badge is not an object`
2380
+ };
2381
+ if (declared === void 0) return void 0;
2382
+ const text = localizedBadgeText(declared.text);
2383
+ if (text === void 0) return {
2384
+ kind: "invalid",
2385
+ reason: `${OWN_METADATA}.badge.text is not a string or an object of lowercase language ids including "en", each text 1 to 12 characters on one line`
2386
+ };
2387
+ return {
2388
+ kind: "badge",
2389
+ badge: {
2390
+ text,
2391
+ tone: BADGE_TONES.find((candidate) => candidate === declared.tone) ?? "neutral"
2392
+ }
2393
+ };
2394
+ }
2395
+ function capabilitiesOf(entry) {
2396
+ const messages = fieldsOf(fieldsOf(entry.api_capabilities)?.anthropic_messages);
2397
+ if (messages === void 0) return {
2398
+ usable: false,
2399
+ reason: "declares no api_capabilities.anthropic_messages"
2400
+ };
2401
+ if ((entry.output_modalities === void 0 ? ["text"] : knownModalities(entry.output_modalities))?.includes("text") !== true) return {
2402
+ usable: false,
2403
+ reason: "declares no text output"
2404
+ };
2405
+ const inputs = entry.input_modalities === void 0 ? ["text"] : knownModalities(entry.input_modalities);
2406
+ if (inputs === void 0 || inputs.length === 0) return {
2407
+ usable: false,
2408
+ reason: "declares no input modality DeepSeek Harness supports"
2409
+ };
2410
+ const contextWindow = capacity(entry.context_window);
2411
+ const maxOutputTokens = capacity(entry.max_output_tokens);
2412
+ if (contextWindow === false || maxOutputTokens === false) return {
2413
+ usable: false,
2414
+ reason: "declares an invalid context_window or max_output_tokens"
2415
+ };
2416
+ const promptUpdate = messages.system_prompt_update ?? void 0;
2417
+ const toolUpdate = messages.tool_update ?? void 0;
2418
+ return {
2419
+ usable: true,
2420
+ facts: {
2421
+ inputModalities: inputs,
2422
+ ...contextWindow === void 0 ? {} : { contextWindow },
2423
+ ...maxOutputTokens === void 0 ? {} : { maxOutputTokens },
2424
+ ...promptUpdate === void 0 ? {} : { systemPromptUpdate: promptUpdate === "in-history" ? "in-history" : "leading-only" },
2425
+ ...toolUpdate === void 0 ? {} : { toolUpdate: toolUpdate === "in-history" || toolUpdate === "addition-only" ? toolUpdate : "leading-only" }
2426
+ }
2427
+ };
2428
+ }
2429
+ /**
2430
+ * Decode one `/models` body entry by entry.
2431
+ * @param body - parsed JSON response.
2432
+ * @returns the entries with a usable id, in listing order.
2433
+ * @throws LlmError `INVALID_RESPONSE` when the body has no `data` array or no usable entry.
2434
+ */
2435
+ function parseModelListing(body) {
2436
+ const data = fieldsOf(body)?.data;
2437
+ if (!Array.isArray(data)) throw new LlmError("DeepSeek model listing has no \"data\" array", "INVALID_RESPONSE");
2438
+ const rows = data;
2439
+ const entries = [];
2440
+ const seen = /* @__PURE__ */ new Set();
2441
+ for (const row of rows) {
2442
+ const entry = fieldsOf(row);
2443
+ const id = entry?.id;
2444
+ if (entry === void 0 || typeof id !== "string" || id.length === 0 || seen.has(id)) continue;
2445
+ seen.add(id);
2446
+ const badge = badgeOf(entry);
2447
+ entries.push({
2448
+ id,
2449
+ ...typeof entry.name === "string" && entry.name.length > 0 ? { name: entry.name } : {},
2450
+ requirement: requirementOf(entry),
2451
+ capabilities: capabilitiesOf(entry),
2452
+ ...badge === void 0 ? {} : { badge }
2453
+ });
2454
+ }
2455
+ if (entries.length === 0) throw new LlmError("DeepSeek model listing contains no model entry", "INVALID_RESPONSE");
2456
+ return {
2457
+ entries,
2458
+ skipped: rows.length - entries.length
2459
+ };
2460
+ }
2461
+ /**
2462
+ * Request and decode one model listing. Redirects fail, so credentials stay on the requested origin.
2463
+ * @param url - listing endpoint.
2464
+ * @param headers - authentication headers for this endpoint.
2465
+ * @param signal - cancellation and deadline of the whole exchange.
2466
+ * @returns the decoded listing.
2467
+ * @throws LlmError `TIMEOUT` when a {@link LISTING_TIMEOUT} deadline elapses before a complete body, `ABORTED` for
2468
+ * another abort, `TRANSPORT` for a network failure, `HTTP_<status>` for a non-2xx answer,
2469
+ * and `INVALID_RESPONSE` for a body that is not a JSON listing.
2470
+ */
2471
+ async function requestModelListing(url, headers, signal) {
2472
+ let response;
2473
+ let text;
2474
+ try {
2475
+ response = await fetch(url, {
2476
+ method: "GET",
2477
+ redirect: "error",
2478
+ signal,
2479
+ headers: {
2480
+ ...attributionHeaders(),
2481
+ accept: "application/json",
2482
+ ...headers
2483
+ }
2484
+ });
2485
+ text = await response.text();
2486
+ } catch (error) {
2487
+ const expired = timeoutOf(signal, LISTING_TIMEOUT);
2488
+ if (expired !== void 0) throw new LlmError(`DeepSeek model listing request to ${url} timed out after ${expired.timeoutMs} ms`, "TIMEOUT", { cause: error });
2489
+ throw signal.aborted ? new LlmError(`DeepSeek model listing request to ${url} was aborted`, "ABORTED", { cause: error }) : new LlmError(`could not reach ${url}`, "TRANSPORT", { cause: error });
2490
+ }
2491
+ if (!response.ok) throw new LlmError(`${url} answered HTTP ${response.status}`, `HTTP_${response.status}`, { status: response.status });
2492
+ let body;
2493
+ try {
2494
+ body = JSON.parse(text);
2495
+ } catch (error) {
2496
+ throw new LlmError(`${url} did not answer with JSON`, "INVALID_RESPONSE", { cause: error });
2497
+ }
2498
+ return parseModelListing(body);
2499
+ }
2500
+ //#endregion
2501
+ //#region lib/types/listing-cache.js
2502
+ /** Credential-scoped `/models` cache with single-flight loads, background refresh, and last-known-good retention. */
2503
+ /** Wait for `task` unless `signal` aborts first; the task keeps running. */
2504
+ async function abandonable(task, signal) {
2505
+ if (signal === void 0) return task;
2506
+ const abandoned = Promise.withResolvers();
2507
+ const abandon = () => {
2508
+ abandoned.reject(signal.reason);
2509
+ };
2510
+ signal.addEventListener("abort", abandon, { once: true });
2511
+ try {
2512
+ await Promise.race([task, abandoned.promise]);
2513
+ } finally {
2514
+ signal.removeEventListener("abort", abandon);
2515
+ }
2516
+ }
2517
+ /** One route's listing cache; each endpoint and credential pair has its own scope. */
2518
+ var ModelListingCache = class {
2519
+ dependencies;
2520
+ scope;
2521
+ listing;
2522
+ /** Settle time of the latest load in this scope; `undefined` before the first one settles. */
2523
+ settledAt;
2524
+ inflight;
2525
+ request;
2526
+ generation = 0;
2527
+ failing = false;
2528
+ closed = false;
2529
+ timer;
2530
+ pending = /* @__PURE__ */ new Set();
2531
+ /** @param dependencies - credential, transport, clock, and diagnostic callbacks. */
2532
+ constructor(dependencies) {
2533
+ this.dependencies = dependencies;
2534
+ }
2535
+ /**
2536
+ * The last published view, without I/O.
2537
+ * @returns the credential state and listing of the current scope.
2538
+ */
2539
+ current() {
2540
+ return {
2541
+ authenticated: this.scope !== void 0,
2542
+ listing: this.listing
2543
+ };
2544
+ }
2545
+ /**
2546
+ * Resolve the view for the current credential. The first access of a scope
2547
+ * waits for its load, which the request deadline bounds; later accesses
2548
+ * return at once and refresh a stale listing in the background. After a load settles,
2549
+ * automatic refresh uses the latest route settings; failures retain the last successful listing, including badges.
2550
+ * @param settings - endpoint and timing of the caller's configuration generation.
2551
+ * @param signal - abandons this wait only; the shared load continues.
2552
+ * @returns the view after any first load of the scope settles.
2553
+ */
2554
+ async view(settings, signal) {
2555
+ const credential = await this.dependencies.credential(settings.url);
2556
+ signal?.throwIfAborted();
2557
+ if (this.closed) return this.current();
2558
+ const scope = credential === void 0 ? void 0 : `${settings.url}\n${createHash("sha256").update(credential).digest("hex")}`;
2559
+ if (scope !== this.scope) this.rescope(scope);
2560
+ if (credential !== void 0) {
2561
+ if (this.settledAt === void 0) await abandonable(this.start(settings, credential), signal);
2562
+ else if (this.dependencies.now() - this.settledAt >= settings.maxAgeMs) this.start(settings, credential);
2563
+ }
2564
+ return this.current();
2565
+ }
2566
+ /** Discard the listing and abort any in-flight load; the next access loads again and waits like a first access. */
2567
+ invalidate() {
2568
+ this.rescope(this.scope);
2569
+ }
2570
+ /**
2571
+ * Stop automatic refresh and abort loads; later results and accesses change nothing.
2572
+ * @returns completion of all admitted loads and automatic credential lookups.
2573
+ */
2574
+ async dispose() {
2575
+ this.closed = true;
2576
+ this.generation += 1;
2577
+ clearTimeout(this.timer);
2578
+ this.request?.abort();
2579
+ await Promise.allSettled(this.pending);
2580
+ }
2581
+ rescope(scope) {
2582
+ clearTimeout(this.timer);
2583
+ this.request?.abort();
2584
+ this.scope = scope;
2585
+ this.listing = void 0;
2586
+ this.settledAt = void 0;
2587
+ this.inflight = void 0;
2588
+ this.failing = false;
2589
+ this.generation += 1;
2590
+ }
2591
+ start(settings, credential) {
2592
+ if (this.inflight !== void 0) return this.inflight;
2593
+ clearTimeout(this.timer);
2594
+ this.generation += 1;
2595
+ const generation = this.generation;
2596
+ const request = new AbortController();
2597
+ this.request = request;
2598
+ const limit = deadline(request.signal, settings.timeoutMs, LISTING_TIMEOUT);
2599
+ const run = this.dependencies.load(settings.url, credential, limit.signal).then((listing) => {
2600
+ if (generation !== this.generation) return;
2601
+ const changed = !deepEqualJson(this.listing, listing);
2602
+ this.listing = listing;
2603
+ this.settledAt = this.dependencies.now();
2604
+ this.failing = false;
2605
+ this.dependencies.info(`model listing from ${settings.url} loaded ${listing.entries.length} model(s), skipped ${listing.skipped} row(s)`);
2606
+ if (changed) this.notify();
2607
+ }, (error) => {
2608
+ if (generation !== this.generation) return;
2609
+ if (!this.failing) {
2610
+ const fallback = this.listing === void 0 ? "using the bundled catalog" : "keeping the previous catalog";
2611
+ this.dependencies.warn(`model listing from ${settings.url} failed (${String(error)}); ${fallback}`);
2612
+ }
2613
+ this.settledAt = this.dependencies.now();
2614
+ this.failing = true;
2615
+ }).finally(() => {
2616
+ limit[Symbol.dispose]();
2617
+ this.pending.delete(run);
2618
+ if (this.inflight === run) this.inflight = void 0;
2619
+ if (generation === this.generation) this.schedule(settings);
2620
+ });
2621
+ this.inflight = run;
2622
+ this.pending.add(run);
2623
+ return run;
2624
+ }
2625
+ notify() {
2626
+ try {
2627
+ this.dependencies.changed();
2628
+ } catch (error) {
2629
+ this.dependencies.warn(`model listing change notification failed (${String(error)}); reload the model catalog`);
2630
+ }
2631
+ }
2632
+ schedule(settings) {
2633
+ clearTimeout(this.timer);
2634
+ if (this.closed || settings.maxAgeMs === 0) return;
2635
+ const age = this.settledAt === void 0 ? 0 : this.dependencies.now() - this.settledAt;
2636
+ this.timer = setTimeout(() => {
2637
+ const operation = this.refresh(settings).finally(() => {
2638
+ this.pending.delete(operation);
2639
+ });
2640
+ this.pending.add(operation);
2641
+ }, Math.min(MAX_TIMER_DELAY_MS, Math.max(1, settings.maxAgeMs - age)));
2642
+ this.timer.unref();
2643
+ }
2644
+ async refresh(previous) {
2645
+ let settings = previous;
2646
+ try {
2647
+ const next = this.dependencies.settings();
2648
+ if (next === void 0) {
2649
+ this.invalidate();
2650
+ this.notify();
2651
+ return;
2652
+ }
2653
+ settings = next;
2654
+ const wasAuthenticated = this.current().authenticated;
2655
+ await this.view(settings);
2656
+ if (this.closed) return;
2657
+ if (wasAuthenticated && !this.current().authenticated) this.notify();
2658
+ } catch (error) {
2659
+ if (this.closed) return;
2660
+ if (!this.failing) this.dependencies.warn(`model listing refresh failed (${String(error)}); keeping the previous catalog; check listing configuration and credentials`);
2661
+ this.failing = true;
2662
+ this.settledAt = this.dependencies.now();
2663
+ }
2664
+ if (this.inflight === void 0) this.schedule(settings);
2665
+ }
2666
+ };
2667
+ //#endregion
2668
+ //#region lib/types/host.js
2669
+ /**
2670
+ * Own a route's listing refreshes, stderr diagnostics, and catalog-change notifications.
2671
+ * @param ctx - provider plugin lifetime.
2672
+ * @param owner - plugin name used in diagnostics.
2673
+ * @param provider - route whose catalog changes.
2674
+ * @param dependencies - current configuration, credentials, and listing transport.
2675
+ * @returns the cache, disposed with the provider after its active work settles.
2676
+ */
2677
+ function createModelListingCache(ctx, owner, provider, dependencies) {
2678
+ const tag = `[${owner}]`;
2679
+ const cache = new ModelListingCache({
2680
+ ...dependencies,
2681
+ now: () => Date.now(),
2682
+ info: (message) => {
2683
+ console.error(tag, message);
2684
+ },
2685
+ warn: (message) => {
2686
+ console.warn(tag, message);
2687
+ },
2688
+ changed: () => {
2689
+ ctx.emit("llm/models-updated", provider);
2690
+ }
2691
+ });
2692
+ ctx.effect(() => () => cache.dispose(), `${owner}: model listing`);
2693
+ return cache;
2694
+ }
2695
+ /**
2696
+ * Register one provider with request-local transport services and live retry policy.
2697
+ * @param ctx - provider plugin lifetime with the LLM registry injected.
2698
+ * @param provider - exact route owned by this plugin.
2699
+ * @param dependencies - provider-owned discovery, credential, and configuration callbacks.
2700
+ */
2701
+ function registerDeepSeekProvider(ctx, provider, dependencies) {
2702
+ ctx.inject(["settings"], (child) => {
2703
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
2704
+ });
2705
+ let userId;
2706
+ const degradedReplays = /* @__PURE__ */ new Set();
2707
+ const omittedRoutes = /* @__PURE__ */ new Set();
2708
+ const unacceptedRoutes = /* @__PURE__ */ new Set();
2709
+ const adapter = new DeepSeekAdapter({
2710
+ ...dependencies,
2711
+ resolveUserId: () => userId ??= getOrCreateAnonymousUserId(),
2712
+ onReplayDegrade: ({ provider, model, reason }) => {
2713
+ const key = `${provider}/${model}\u0000${reason}`;
2714
+ if (degradedReplays.has(key)) return;
2715
+ degradedReplays.add(key);
2716
+ ctx.logger.warn(`llm-deepseek: unusable Messages replay state on assistant history for route "${provider}/${model}"; sending provider-neutral content (${reason})`);
2717
+ },
2718
+ onExtensionsOmitted: ({ provider, model, fields, error }) => {
2719
+ const route = `${provider}/${model}`;
2720
+ if (omittedRoutes.has(route)) return;
2721
+ omittedRoutes.add(route);
2722
+ ctx.logger.warn(`llm-deepseek: sending route "${route}" without request extension fields ${fields.join(", ")} because they failed to serialize: %o`, error);
2723
+ },
2724
+ onExtensionsUnaccepted: ({ provider, model, error }) => {
2725
+ const route = `${provider}/${model}`;
2726
+ if (unacceptedRoutes.has(route)) return;
2727
+ unacceptedRoutes.add(route);
2728
+ ctx.logger.warn(`llm-deepseek: route "${route}" request extension acceptance failed; contributors resend on a later request: %o`, error);
2729
+ },
2730
+ resolveAttachments: () => ctx.get("attachments"),
2731
+ resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
2732
+ prepareExtensions: (request) => ctx.get("deepseekLlmApiExtensions")?.prepare(request) ?? Promise.resolve({
2733
+ fields: {},
2734
+ accept: () => Promise.resolve()
2735
+ })
2736
+ });
2737
+ const registration = ctx.llm.registerAdapter([provider], adapter);
2738
+ let registeredPolicy = dependencies.options().retryPolicy;
2739
+ ctx.on("loader/volatile-update", () => {
2740
+ let policy;
2741
+ try {
2742
+ policy = dependencies.options().retryPolicy;
2743
+ } catch (error) {
2744
+ ctx.logger.warn(error);
2745
+ return;
2746
+ }
2747
+ if (deepEqualJson(policy, registeredPolicy)) return;
2748
+ registration.replace([provider]);
2749
+ registeredPolicy = policy;
2750
+ });
2751
+ }
2752
+ Object.defineProperty(registerDeepSeekProvider, "transportSeam", { value: "deepseek-messages-create-transport-v1", enumerable: true });
2753
+ //#endregion
2754
+ //#region lib/types/listing-config.js
2755
+ /** Default deadline of one model-listing request and of a first-use wait. */
2756
+ const DEFAULT_MODELS_TIMEOUT_MS = 5e3;
2757
+ /** Default interval between automatic listing refreshes after first use. */
2758
+ const DEFAULT_MODELS_MAX_AGE_MS = 6e5;
2759
+ /** Schemas of the {@link ModelListingConfig} fields. */
2760
+ const modelListingConfigFields = {
2761
+ modelsTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_MODELS_TIMEOUT_MS).volatile(),
2762
+ modelsMaxAgeMs: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MODELS_MAX_AGE_MS).volatile()
2763
+ };
2764
+ /**
2765
+ * Validate listing timing. Settings updates can reach it without schema validation, so every bound is judged here.
2766
+ * @param options - plain listing options; absent values take the defaults.
2767
+ * @param owner - plugin name that prefixes each failure message.
2768
+ * @returns the validated timing.
2769
+ * @throws Error when a value is outside its schema bounds.
2770
+ */
2771
+ function resolveListingTiming(options, owner) {
2772
+ const { modelsTimeoutMs = DEFAULT_MODELS_TIMEOUT_MS, modelsMaxAgeMs = DEFAULT_MODELS_MAX_AGE_MS } = options;
2773
+ if (!Number.isFinite(modelsTimeoutMs) || modelsTimeoutMs <= 0 || modelsTimeoutMs > MAX_TIMER_DELAY_MS) throw new Error(`${owner}: modelsTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
2774
+ if (!Number.isSafeInteger(modelsMaxAgeMs) || modelsMaxAgeMs < 0) throw new Error(`${owner}: modelsMaxAgeMs must be a non-negative safe integer`);
2775
+ return {
2776
+ timeoutMs: modelsTimeoutMs,
2777
+ maxAgeMs: modelsMaxAgeMs
2778
+ };
2779
+ }
2780
+ //#endregion
2781
+ //#region lib/types/listed-catalog.js
2782
+ /** A listed catalog: version gating, this build's model knowledge, and deployment overrides. */
2783
+ /** Replace fields of an entry; a text-only result drops the image limits it inherited. */
2784
+ function layered(model, layer) {
2785
+ const merged = {
2786
+ ...model,
2787
+ ...layer
2788
+ };
2789
+ if (merged.inputModalities?.includes("image") !== true) {
2790
+ if (layer.imagePixelBudget === void 0) delete merged.imagePixelBudget;
2791
+ if (layer.imageMaxBytes === void 0) delete merged.imageMaxBytes;
2792
+ }
2793
+ return merged;
2794
+ }
2795
+ /** Layer one usable listing entry over this build's entry for the same id; a declared `leading-only` mode removes the inherited one. */
2796
+ function listed(base, entry, facts, routeMaxTokens) {
2797
+ const { systemPromptUpdate, toolUpdate, ...rest } = base;
2798
+ return layered({
2799
+ ...rest,
2800
+ ...systemPromptUpdate === void 0 || facts.systemPromptUpdate === "leading-only" ? {} : { systemPromptUpdate },
2801
+ ...toolUpdate === void 0 || facts.toolUpdate === "leading-only" ? {} : { toolUpdate }
2802
+ }, {
2803
+ ...entry.name === void 0 ? {} : { name: entry.name },
2804
+ ...facts.contextWindow === void 0 ? {} : { contextWindow: facts.contextWindow },
2805
+ ...facts.maxOutputTokens === void 0 ? {} : { maxTokens: Math.min(routeMaxTokens, facts.maxOutputTokens) },
2806
+ ...facts.systemPromptUpdate === "in-history" ? { systemPromptUpdate: "in-history" } : {},
2807
+ ...facts.toolUpdate === void 0 || facts.toolUpdate === "leading-only" ? {} : { toolUpdate: facts.toolUpdate },
2808
+ inputModalities: [...facts.inputModalities]
2809
+ });
2810
+ }
2811
+ function gateOf(entry, known, clientVersion, problems) {
2812
+ const requirement = entry.requirement;
2813
+ switch (requirement.kind) {
2814
+ case "none": return known ? void 0 : { kind: "unsupported" };
2815
+ case "min-version":
2816
+ if (semver.valid(requirement.version) === null) break;
2817
+ return semver.gte(clientVersion, requirement.version) ? void 0 : {
2818
+ kind: "upgrade-required",
2819
+ requiredVersion: requirement.version
2820
+ };
2821
+ case "invalid": break;
2822
+ /* v8 ignore next 2 -- listing decoding produces only the closed requirement union */
2823
+ default: return assertNever(requirement, "model listing client requirement");
2824
+ }
2825
+ problems.push(`model "${entry.id}" declares an unreadable client requirement`);
2826
+ return { kind: "unsupported" };
2827
+ }
2828
+ /**
2829
+ * Resolve a route's catalog from its listing. A declared minimum client version decides a
2830
+ * listed model's availability; without one, only models this build supports
2831
+ * are offered and the other listed models stay hidden.
2832
+ * @param input - listing, supported models, overrides, route cap, and this build's version.
2833
+ * @returns offered models, request-resolution entries, refusals, and diagnostics.
2834
+ */
2835
+ function resolveListedCatalog(input) {
2836
+ const supported = new Map(input.supported.map((model) => [model.id, model]));
2837
+ const problems = [];
2838
+ const overrideErrors = [];
2839
+ const withOverride = (model) => {
2840
+ const override = input.overrides[model.id];
2841
+ if (override === void 0) return model;
2842
+ const candidate = layered(model, override);
2843
+ try {
2844
+ resolveAdapterOptions({ models: [candidate] });
2845
+ } catch (error) {
2846
+ overrideErrors.push(`modelOverrides.${model.id} does not apply (${String(error)})`);
2847
+ return model;
2848
+ }
2849
+ return candidate;
2850
+ };
2851
+ if (input.listing === void 0) {
2852
+ const resolution = resolveAdapterOptions({ models: input.supported.map(withOverride) }).models;
2853
+ return {
2854
+ offered: resolution,
2855
+ resolution,
2856
+ refusals: /* @__PURE__ */ new Map(),
2857
+ badges: /* @__PURE__ */ new Map(),
2858
+ problems,
2859
+ overrideErrors
2860
+ };
2861
+ }
2862
+ const offered = [];
2863
+ const refusals = /* @__PURE__ */ new Map();
2864
+ const badges = /* @__PURE__ */ new Map();
2865
+ for (const entry of input.listing) {
2866
+ const refusal = gateOf(entry, supported.has(entry.id), input.clientVersion, problems);
2867
+ if (refusal !== void 0) {
2868
+ refusals.set(entry.id, refusal);
2869
+ continue;
2870
+ }
2871
+ if (!entry.capabilities.usable) {
2872
+ problems.push(`model "${entry.id}" ${entry.capabilities.reason}`);
2873
+ refusals.set(entry.id, { kind: "unsupported" });
2874
+ continue;
2875
+ }
2876
+ offered.push(withOverride(listed(supported.get(entry.id) ?? { id: entry.id }, entry, entry.capabilities.facts, input.routeMaxTokens)));
2877
+ const badge = entry.badge;
2878
+ switch (badge?.kind) {
2879
+ case void 0: break;
2880
+ case "badge":
2881
+ badges.set(entry.id, badge.badge);
2882
+ break;
2883
+ case "invalid":
2884
+ problems.push(`model "${entry.id}" shows no badge: ${badge.reason}`);
2885
+ break;
2886
+ /* v8 ignore next 2 -- listing decoding produces only the closed badge union */
2887
+ default: assertNever(badge, "listed badge");
2888
+ }
2889
+ }
2890
+ const offeredIds = new Set(offered.map((model) => model.id));
2891
+ const listedIds = new Set(input.listing.map((entry) => entry.id));
2892
+ for (const id of Object.keys(input.overrides)) if (!listedIds.has(id) && !supported.has(id)) overrideErrors.push(`modelOverrides names "${id}", which neither the model listing nor the built-in catalog describes`);
2893
+ const resolution = resolveAdapterOptions({ models: [...offered, ...input.supported.filter((model) => !offeredIds.has(model.id)).map(withOverride)] }).models;
2894
+ return {
2895
+ offered: resolution.slice(0, offered.length),
2896
+ resolution,
2897
+ refusals,
2898
+ badges,
2899
+ problems,
2900
+ overrideErrors
2901
+ };
2902
+ }
2903
+ /**
2904
+ * Refuse a listed model this build cannot serve, before any network I/O.
2905
+ * @param model - requested wire model id.
2906
+ * @param refusal - the catalog's refusal for `model`, or `undefined` when it is admitted.
2907
+ * @throws LlmError `CLIENT_UPGRADE_REQUIRED` naming the required and current versions, or `MODEL_UNSUPPORTED`.
2908
+ */
2909
+ function refuseListedModel(model, refusal) {
2910
+ if (refusal === void 0) return;
2911
+ switch (refusal.kind) {
2912
+ case "upgrade-required": throw new LlmError(`DeepSeek model "${model}" requires DeepSeek Harness ${refusal.requiredVersion} or later; current version is ${APP_IDENTITY.version}`, "CLIENT_UPGRADE_REQUIRED");
2913
+ case "unsupported": throw new LlmError(`DeepSeek model "${model}" is not supported by DeepSeek Harness; current version is ${APP_IDENTITY.version}`, "MODEL_UNSUPPORTED");
2914
+ /* v8 ignore next 2 -- catalog resolution produces only the closed refusal union */
2915
+ default: assertNever(refusal, "listed catalog refusal");
2916
+ }
2917
+ }
2918
+ //#endregion
2919
+ export { Config, DEFAULT_CONTEXT_WINDOW, DEFAULT_FILES_API_TIMEOUT_MS, DEFAULT_FILE_EXPIRY_SECONDS, DEFAULT_FILE_QUOTA_CLEANUP_BATCH, DEFAULT_FILE_REFRESH_MARGIN_SECONDS, DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM, DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM, DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM, DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET, DEFAULT_MAX_IMAGES_PER_REQUEST, DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES, DEFAULT_MAX_REQUEST_FILES_BYTES, DEFAULT_MAX_TOKENS, DEFAULT_MODELS_MAX_AGE_MS, DEFAULT_MODELS_TIMEOUT_MS, DEFAULT_REQUEST_IMAGE_MAX_BYTES, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DeepSeekAdapter, DeepSeekFileId, DeepSeekFileStore, DeepSeekFilesClient, DeepSeekUploadIndex, LISTING_TIMEOUT, MAX_FILE_EXPIRY_SECONDS, MAX_FILE_UPLOAD_BYTES, MAX_IMAGE_BYTES, MAX_STORED_FILE_BYTES, MAX_STORED_FILE_COUNT, MIN_FILE_EXPIRY_SECONDS, ModelListingCache, PUBLIC_BASE_URL, REQUEST_IMAGE_MAX_DIMENSION, catalogModelFields, catalogModelInfo, createModelListingCache, deepSeekConfigFields, deepSeekFileScope, deepSeekImageRequestPricing, deepSeekImageTokens, deepSeekRequestImageDimensions, isBuiltInCatalog, modelListingConfigFields, modelListingURL, parseModelListing, plainOptions, refuseListedModel, registerDeepSeekProvider, requestModelListing, resolveAdapterOptions, resolveListedCatalog, resolveListingTiming, resolveRequestImageMaxBytes, resolveRequestImageTarget };