@toddzheng024/dscode-bundle 0.7.26 → 0.7.28

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 (99) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +10 -10
  3. package/bootstrap.mjs +1 -1
  4. package/cordis.patch.yml +418 -45
  5. package/package.json +279 -245
  6. package/plugins/auto-review/index.mjs +8 -5
  7. package/plugins/code-review/index.mjs +1 -1
  8. package/plugins/computer-use/index.mjs +37 -0
  9. package/plugins/cordis-config/plain.mjs +31 -0
  10. package/plugins/exec/cli.mjs +1 -1
  11. package/plugins/grok/index.mjs +28 -24
  12. package/plugins/memory/index.mjs +1 -1
  13. package/plugins/message-source/kind.mjs +20 -0
  14. package/plugins/openrouter/adapter.mjs +29 -13
  15. package/plugins/openrouter/index.mjs +31 -28
  16. package/plugins/session-bridge/communication.mjs +6 -3
  17. package/plugins/session-bridge/server.mjs +3 -2
  18. package/plugins/session-bridge/tasks.mjs +3 -1
  19. package/plugins/session-cards/index.mjs +1 -1
  20. package/plugins/time-marks/index.mjs +3 -2
  21. package/plugins/time-marks/marks.mjs +6 -5
  22. package/plugins/triggers/config.mjs +1 -1
  23. package/plugins/triggers/tools.mjs +1 -1
  24. package/plugins/tui-tools/doctor.mjs +1 -1
  25. package/plugins/tui-tools/index.mjs +16 -4
  26. package/plugins/ultra/policy.mjs +16 -0
  27. package/vendor/bash/index.js +455 -186
  28. package/vendor/bash/types/background.d.ts +38 -7
  29. package/vendor/bash/types/index.d.ts +20 -4
  30. package/vendor/bash/types/render.d.ts +28 -9
  31. package/vendor/command-goal/index.js +3 -1
  32. package/vendor/deepseek/index.js +1378 -1246
  33. package/vendor/deepseek/types/adapter.d.ts +16 -155
  34. package/vendor/deepseek/types/config.d.ts +127 -0
  35. package/vendor/deepseek/types/defaults.d.ts +24 -0
  36. package/vendor/deepseek/types/file-store.d.ts +5 -3
  37. package/vendor/deepseek/types/files-api.d.ts +12 -9
  38. package/vendor/deepseek/types/image-tokens.d.ts +24 -9
  39. package/vendor/deepseek/types/images.d.ts +35 -0
  40. package/vendor/deepseek/types/index.d.ts +10 -94
  41. package/vendor/deepseek/types/messages-api.d.ts +10 -0
  42. package/vendor/deepseek/types/model-info.d.ts +16 -0
  43. package/vendor/deepseek/types/models.d.ts +4 -0
  44. package/vendor/deepseek/types/replay.d.ts +27 -0
  45. package/vendor/deepseek/types/request-extensions.d.ts +16 -0
  46. package/vendor/deepseek/types/request-files.d.ts +49 -0
  47. package/vendor/deepseek/types/request-pricing.d.ts +32 -20
  48. package/vendor/deepseek/types/serialize.d.ts +17 -82
  49. package/vendor/deepseek/types/sse.d.ts +6 -22
  50. package/vendor/deepseek/types/translate.d.ts +13 -36
  51. package/vendor/deepseek/types/transport.d.ts +15 -0
  52. package/vendor/deepseek/types/types.d.ts +106 -167
  53. package/vendor/deepseek/types/wire-types.d.ts +62 -0
  54. package/vendor/persistent/index.js +9 -3
  55. package/vendor/subagent/index.js +11 -8
  56. package/vendor/subagent/model-selection-settings.js +12 -38
  57. package/vendor/subagent/types/index.d.ts +4 -3
  58. package/vendor/subagent/types/model-selection-settings.d.ts +12 -10
  59. package/vendor/subagent-core/index.js +401 -264
  60. package/vendor/subagent-core/typert.host.js +103 -113
  61. package/vendor/subagent-core/typert.remote-client.d.ts +1 -3
  62. package/vendor/subagent-core/typert.remote-client.js +23 -69
  63. package/vendor/subagent-core/types/archive-admission.d.ts +16 -0
  64. package/vendor/subagent-core/types/archive-admission.js +151 -0
  65. package/vendor/subagent-core/types/assistant-output.d.ts +2 -2
  66. package/vendor/subagent-core/types/catalog.d.ts +17 -10
  67. package/vendor/subagent-core/types/catalog.js +12 -6
  68. package/vendor/subagent-core/types/child-agent.d.ts +8 -5
  69. package/vendor/subagent-core/types/child-agent.js +11 -5
  70. package/vendor/subagent-core/types/continuation-activation.d.ts +17 -1
  71. package/vendor/subagent-core/types/continuation-activation.js +41 -4
  72. package/vendor/subagent-core/types/continuation-messages.d.ts +1 -1
  73. package/vendor/subagent-core/types/continuation-messages.js +7 -3
  74. package/vendor/subagent-core/types/continuation.d.ts +1 -1
  75. package/vendor/subagent-core/types/continuation.js +2 -2
  76. package/vendor/subagent-core/types/control-types.d.ts +29 -36
  77. package/vendor/subagent-core/types/control-types.js +2 -3
  78. package/vendor/subagent-core/types/control.d.ts +2 -28
  79. package/vendor/subagent-core/types/control.js +3 -40
  80. package/vendor/subagent-core/types/index.d.ts +45 -41
  81. package/vendor/subagent-core/types/index.js +46 -82
  82. package/vendor/subagent-core/types/internal.d.ts +2 -2
  83. package/vendor/subagent-core/types/internal.js +2 -2
  84. package/vendor/subagent-core/types/lifecycle.d.ts +1 -1
  85. package/vendor/subagent-core/types/lifecycle.js +5 -3
  86. package/vendor/subagent-core/types/list-children.d.ts +15 -32
  87. package/vendor/subagent-core/types/list-children.js +45 -49
  88. package/vendor/subagent-core/types/out-of-process.d.ts +1 -1
  89. package/vendor/subagent-core/types/projection-types.d.ts +9 -1
  90. package/vendor/subagent-core/types/projection.d.ts +5 -0
  91. package/vendor/subagent-core/types/projection.js +10 -4
  92. package/vendor/subagent-core/types/run-settlement.js +1 -1
  93. package/vendor/subagent-core/types/types.d.ts +2 -2
  94. package/vendor/terminal/index.js +94 -30
  95. package/vendor/tui/lib/index.mjs +2 -1
  96. package/vendor/tui/lib/kernel-panels.mjs +1 -1
  97. package/vendor/tui/lib/render/projection.mjs +50 -36
  98. package/presets/dscode/agent.cordis.yml +0 -321
  99. package/presets/dscode/preset.yml +0 -3
@@ -1,542 +1,105 @@
1
- // dscode-ultra-v1
1
+ // dscode-ultra-v2
2
2
  const ULTRA_POLICY = "DSCODE ULTRA — max reasoning with task-proportional execution.\nUse the depth needed to resolve actual uncertainty. Ultra is capability available on demand, not a requirement to maximize investigation, planning, delegation or verification. Briefly choose the smallest sufficient approach, then act. Do not repeatedly reassess a decision without new evidence.\nFor a bounded task such as adding a unit test, a small bug fix or a local edit: work directly in the parent. Read the target implementation, applicable instructions and a nearby relevant example; make the requested change; run the focused test and required project checks; fix observed failures; then report the result and stop. Do not scan the whole repository, add a formal plan, launch reviewers, broaden coverage or refactor unrelated code unless concrete evidence makes it necessary. Once acceptance criteria and required checks pass, do not invent additional work or rerun passing checks without a relevant change. If the task turns out to involve an unclear contract, a broad regression or a shared interface, expand only to resolve that specific uncertainty.\nWhen delegating, explicitly choose reasoning_effort for each child instead of automatically propagating ultra. Prefer low for bounded implementation, unit tests and factual lookup; high for nontrivial debugging or review; max for exceptional uncertainty or complex design. These are guidelines, not a substitute for judging the task. Use only efforts supported by the child model. Omission inherits the parent; choosing a child effort never changes the parent effort. Both subagent and subagent_fork support effort-only selection.\nFor substantial tasks, delegate only independent work that is likely to shorten completion or resolve meaningful uncertainty. Before delegating, identify the independent boundary, concrete wall-clock benefit, and useful work you will do while the child runs. Give each child a bounded objective, relevant context, file ownership and acceptance criteria. Give each child a unique name (1-10 characters, letters, digits and underscores, starting and ending with a letter, such as read_code) and address it as /name in send_message and interrupt_agent; a child addresses you as /. Prefer subagent_fork when established conversation history is relevant; use fresh subagent for self-contained work that does not benefit from that history. Fork excludes the current unfinished turn, so always give a self-contained assignment. Keep useful work for yourself while children run. For read-only work or tasks needing the parent's uncommitted files, omit worktree and assign disjoint files if writing. For independent parallel edits on a clean repository, set worktree: true; the child starts at HEAD in an isolated checkout. Never have multiple agents edit the same files in a shared workspace. Inspect and integrate worktree changes before removing the checkout.\nIn ultra use subagent/subagent_fork and send_message for delegation, not workflow or ralph. Use at most three child agents concurrently across this root session. Children complete their assigned work themselves and cannot delegate again; do not duplicate investigations across agents. The parent owns integration, verifies child claims, resolves conflicts and runs appropriate checks. Seek independent review of substantial changes when useful; do not add a review round merely because ultra is enabled. Parent/child messages are available; sibling direct messaging is not. Preserve the user's permission policy: ultra grants no extra authority. Reuse findings and stop delegating when coordination costs outweigh value. If progress stalls, name the concrete blocker and take the next diagnostic step rather than silently extending deliberation.";
3
- function ultraRequest(options, messages) {
4
- if (options.reasoningEffort !== 'ultra' || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return messages;
5
- const copy = messages.map(m => ({ ...m }));
6
- const system = copy.findLast(m => m.role === 'system' && typeof m.content === 'string');
7
- if (system) system.content += '\n\n' + ULTRA_POLICY;
8
- else copy.unshift({ role: 'system', content: ULTRA_POLICY });
9
- return copy;
3
+ function ultraSystem(options, system) {
4
+ if (options.reasoningEffort !== 'ultra' || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return system;
5
+ return system.length === 0 ? ULTRA_POLICY : system + '\n\n' + ULTRA_POLICY;
10
6
  }
11
7
  const FLASH_POLICY = "DSCODE DeepSeek Flash — use task-proportional effort. For a simple question, answer directly. For a bounded coding change, read the relevant code, make the change, run the focused check, and stop when it passes. Avoid repeated planning, broad repository scans, speculative edge cases, extra review rounds, or repeated tests unless a concrete failure or uncertainty calls for them. Keep explanations concise while reporting the result and any real limitation.";
12
- function flashRequest(options, messages) {
13
- if (options.model !== 'deepseek-flash' || options.purpose || !options.sessionId) return messages;
14
- const copy = messages.map(m => ({ ...m }));
15
- const system = copy.findLast(m => m.role === 'system' && typeof m.content === 'string');
16
- if (system) system.content += '\n\n' + FLASH_POLICY;
17
- else copy.unshift({ role: 'system', content: FLASH_POLICY });
18
- return copy;
8
+ function flashSystem(options, system) {
9
+ if (options.model !== 'deepseek-flash' || options.purpose || !options.sessionId) return system;
10
+ return system.length === 0 ? FLASH_POLICY : system + '\n\n' + FLASH_POLICY;
19
11
  }
20
- import z from "@deepseek-ai/schemastery";
21
- import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, RetryPolicySchema, assertUsableApiKey, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError, offloadRequestImagesWithPolicy, offloadedImagePrefixCount, offloadedImageText, requestImageHandleText, resolveImageAttachmentAccess, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
22
- import { credentialRef } from "@deepseek-ai/dsh-credentials";
12
+ import { IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, RetryPolicySchema, ToolCallId, assertUsableApiKey, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError, offloadedImageText, projectOffloadedImages, requestImageHandleText, requiredImageOffload, resolveImageAttachmentAccess, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
23
13
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
24
- import { MAX_TIMER_DELAY_MS, deadline, idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
25
14
  import { deepEqualJson } from "@deepseek-ai/dsh-util-values";
26
15
  import { getOrCreateAnonymousUserId } from "@deepseek-ai/dsh-anonymous-user-id";
27
- import { ImageVariantId, requestImageDimensions } from "@deepseek-ai/dsh-attachment";
16
+ import { MAX_TIMER_DELAY_MS, deadline, idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
28
17
  import { createHash } from "node:crypto";
29
18
  import { mkdir, readFile } from "node:fs/promises";
30
19
  import { dirname, join } from "node:path";
31
20
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
32
21
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
22
+ import { ImageVariantId, longEdgeDimensions, requestImageDimensions } from "@deepseek-ai/dsh-attachment";
33
23
  import { EventSourceParserStream } from "eventsource-parser/stream";
34
- import { brandString } from "@deepseek-ai/dsh-brand";
35
- //#region lib/types/serialize.js
36
- /**
37
- * Serialize harness messages into DeepSeek chat completions. Text-only
38
- * requests retain string user content; the image path resolves durable
39
- * attachments into ordered file-id or inline parts. Tool-result images follow their
40
- * string-only tool messages in a separate user message.
41
- * @module dsh-llm-deepseek/serialize
42
- */
43
- const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result:";
44
- /** Validate the adapter-owned effort before resolving its DeepSeek wire fields. */
45
- function reasoningEffort(effort) {
46
- if (effort === "ultra") return "max";
47
- if (effort === "off" || effort === "low" || effort === "high" || effort === "max") return effort;
48
- throw new LlmError(`DeepSeek does not support reasoning effort "${effort}"`, "UNSUPPORTED_REASONING_EFFORT");
49
- }
50
- /** Resolve one legal thinking/effort pair without exposing `off` as a wire effort. */
51
- function resolveThinking(options, defaults) {
52
- if (options.purpose === "session-title") return { thinking: "disabled" };
53
- const effort = options.reasoningEffort === void 0 ? defaults.reasoningEffort : reasoningEffort(options.reasoningEffort);
54
- if (defaults.thinking === "disabled" && effort !== void 0 && effort !== "off") throw new LlmError(`DeepSeek deployment does not support reasoning effort "${effort}"`, "UNSUPPORTED_REASONING_EFFORT");
55
- if (effort === "off") return { thinking: "disabled" };
56
- if (effort === "low" || effort === "high" || effort === "max") return {
57
- thinking: "enabled",
58
- reasoningEffort: effort
59
- };
60
- return defaults.thinking === void 0 ? {} : { thinking: defaults.thinking };
61
- }
62
- /** Join the text blocks of a message (used for user/tool-result content). */
63
- function flattenText(blocks) {
64
- return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
65
- }
66
- /** Reject core image content before any text-flattening path can silently erase it. */
67
- function assertTextOnly(blocks) {
68
- if (contentHasImage(blocks)) throw new LlmError("The DeepSeek chat-completions adapter does not support image content.", "UNSUPPORTED_CONTENT");
69
- }
70
- /** Reject roles whose DeepSeek history format cannot carry image input. */
71
- function assertSupportedImageRoles(messages) {
72
- for (const message of messages) if (message.role !== "user" && contentHasImage(message.content)) throw new LlmError(`The DeepSeek chat-completions adapter cannot represent image content in a ${message.role} message.`, "UNSUPPORTED_CONTENT");
73
- }
74
- /** Describe the exact request preview and its model-callable coordinate system. */
75
- function imageHandle(ref, version, resolveAccess, precededByContent) {
76
- return {
77
- type: "text",
78
- text: `${precededByContent ? "\n" : ""}${requestImageHandleText(ref, version, resolveAccess?.(ref))}`
79
- };
80
- }
81
- /** Resolve one durable image into its descriptor and transient DeepSeek image part. */
82
- async function imageParts(block, images, location, precededByContent) {
83
- const version = images.requestImages.get(block.attachment.attachmentId);
84
- if (version === void 0) throw new LlmError(`DeepSeek request image ${block.attachment.attachmentId} was not prepared.`, "INVALID_REQUEST");
85
- const image = images.representation.kind === "file" ? {
86
- type: "file",
87
- file_id: await images.representation.resolveFileId(version, block, location)
88
- } : {
89
- type: "image_url",
90
- image_url: { url: `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}` }
91
- };
92
- return [imageHandle(block.attachment, version, images.resolveImageAccess, precededByContent), image];
93
- }
94
- /** Convert user or nested tool-result blocks into ordered wire parts. */
95
- async function contentParts(blocks, images, message, nextImage) {
96
- const parts = [];
97
- for (const block of blocks) switch (block.type) {
98
- case "text":
99
- if (block.text.length > 0) parts.push({
100
- type: "text",
101
- text: block.text
102
- });
103
- break;
104
- case "image":
105
- nextImage.value += 1;
106
- parts.push(...await imageParts(block, images, {
107
- message,
108
- image: nextImage.value
109
- }, parts.length > 0));
110
- break;
111
- case "tool-result":
112
- parts.push(...await contentParts(block.content, images, message, nextImage));
113
- break;
114
- default: break;
115
- }
116
- return parts;
117
- }
118
- /** Keep text-only user messages on the compact string wire form. */
119
- function userContent(parts) {
120
- const text = [];
121
- for (const part of parts) {
122
- if (part.type !== "text") return [...parts];
123
- text.push(part.text);
124
- }
125
- return text.join("");
126
- }
127
- /** Serialize one assistant message (text + reasoning + tool calls). */
128
- function serializeAssistant(message) {
129
- const text = flattenText(message.content);
130
- const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
131
- const toolCalls = message.content.filter((block) => block.type === "tool-call").map((block) => ({
132
- id: block.id,
133
- type: "function",
134
- function: {
135
- name: block.name,
136
- arguments: block.arguments
137
- }
138
- }));
139
- return {
140
- role: "assistant",
141
- content: text,
142
- ...reasoning.length > 0 ? { reasoning_content: reasoning } : {},
143
- ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
144
- };
145
- }
146
- /**
147
- * Serialize the conversation. `tool-result` blocks become standalone
148
- * `{role: 'tool'}` messages; the harness puts each tool result in its own
149
- * user-role message, so a mixed user message contributes its text first and
150
- * its tool results as separate wire messages after.
151
- * @param messages - the harness conversation, in order.
152
- * @returns the wire messages; order preserved, each tool result expanded into its own entry.
153
- */
154
- function serializeMessages(messages) {
155
- const wire = [];
156
- for (const message of messages) {
157
- assertTextOnly(message.content);
158
- if (message.role === "system") {
159
- wire.push({
160
- role: "system",
161
- content: flattenText(message.content)
162
- });
163
- continue;
164
- }
165
- if (message.role === "assistant") {
166
- wire.push(serializeAssistant(message));
167
- continue;
168
- }
169
- const toolResults = message.content.filter((block) => block.type === "tool-result");
170
- const text = flattenText(message.content);
171
- if (text.length > 0 || toolResults.length === 0) wire.push({
172
- role: "user",
173
- content: text
174
- });
175
- for (const result of toolResults) wire.push({
176
- role: "tool",
177
- tool_call_id: result.toolCallId,
178
- content: flattenText(result.content) || "(no output)"
179
- });
180
- }
181
- return wire;
182
- }
183
- /**
184
- * Serialize image-capable history after resolving durable attachments.
185
- * Consecutive tool results keep string `tool` messages and share one following
186
- * user message containing their images.
187
- * @param messages - transient request history after request-size offloading.
188
- * @param images - prepared request versions, one provider representation, and its budget.
189
- * @returns ordered DeepSeek wire messages.
190
- */
191
- async function serializeMessagesWithImages(messages, images) {
192
- assertSupportedImageRoles(messages);
193
- const wire = [];
194
- let pendingToolImages = [];
195
- const flushToolImages = () => {
196
- if (pendingToolImages.length === 0) return;
197
- wire.push({
198
- role: "user",
199
- content: [{
200
- type: "text",
201
- text: TOOL_RESULT_IMAGE_TEXT
202
- }, ...pendingToolImages]
203
- });
204
- pendingToolImages = [];
205
- };
206
- for (const [messageIndex, message] of messages.entries()) {
207
- const nextImage = { value: 0 };
208
- if (message.role === "system") {
209
- flushToolImages();
210
- wire.push({
211
- role: "system",
212
- content: flattenText(message.content)
213
- });
214
- continue;
215
- }
216
- if (message.role === "assistant") {
217
- flushToolImages();
218
- wire.push(serializeAssistant(message));
219
- continue;
220
- }
221
- const regular = message.content.filter((block) => block.type !== "tool-result");
222
- const toolResults = message.content.filter((block) => block.type === "tool-result");
223
- const content = userContent(await contentParts(regular, images, messageIndex + 1, nextImage));
224
- if (content.length > 0 || toolResults.length === 0) {
225
- flushToolImages();
226
- wire.push({
227
- role: "user",
228
- content
229
- });
230
- }
231
- for (const result of toolResults) {
232
- const parts = await contentParts(result.content, images, messageIndex + 1, nextImage);
233
- const imageParts = parts.filter((part) => part.type !== "text");
234
- const text = parts.filter((part) => part.type === "text").map((part) => part.text).join("");
235
- wire.push({
236
- role: "tool",
237
- tool_call_id: result.toolCallId,
238
- content: text || "(no output)"
239
- });
240
- pendingToolImages.push(...imageParts);
241
- }
242
- }
243
- flushToolImages();
244
- return wire;
245
- }
246
- /** Assemble request fields shared by text-only and image-capable conversion. */
247
- function requestWithMessages(options, messages, defaults) {
248
- messages = flashRequest(options, messages);
249
- messages = ultraRequest(options, messages);
250
- const tools = options.tools?.filter((tool) => tool.name !== "workflow" && tool.name !== "ralph").map((tool) => ({
251
- type: "function",
252
- function: {
253
- name: tool.name,
254
- description: tool.description,
255
- parameters: tool.parameters
256
- }
257
- }));
258
- const resolvedThinking = resolveThinking(options, defaults);
259
- return {
260
- model: options.model,
261
- messages,
262
- stream: true,
263
- stream_options: { include_usage: true },
264
- ...resolvedThinking.thinking !== void 0 ? { thinking: { type: resolvedThinking.thinking } } : {},
265
- ...resolvedThinking.reasoningEffort !== void 0 ? { reasoning_effort: resolvedThinking.reasoningEffort } : {},
266
- ...tools !== void 0 && tools.length > 0 ? { tools } : {},
267
- ...options.temperature !== void 0 ? { temperature: options.temperature } : {},
268
- ...options.maxTokens === void 0 ? {} : { max_tokens: options.maxTokens },
269
- ...options.stop !== void 0 ? { stop: options.stop } : {}
270
- };
271
- }
272
- /**
273
- * Build the full wire request. Always streaming (`stream: true`, usage
274
- * reporting on); optional fields are omitted rather than sent as null, so
275
- * provider defaults apply.
276
- * @param options - the harness request (model, history, system, tools, sampling).
277
- * @param defaults - adapter-level thinking defaults; undefined fields put nothing on the wire.
278
- * @returns the chat-completions request body.
279
- */
280
- function serializeRequest(options, defaults = {}) {
281
- const messages = [];
282
- if (options.system !== void 0) messages.push({
283
- role: "system",
284
- content: options.system
285
- });
286
- messages.push(...serializeMessages(options.messages));
287
- return requestWithMessages(options, messages, defaults);
288
- }
289
- /**
290
- * Build one image-capable request while keeping durable bytes out of session
291
- * messages. Oversized oldest images become per-image text after their
292
- * exact request-version byte lengths are known and before provider serialization.
293
- * @param options - harness request containing image-capable user content.
294
- * @param images - request versions, optional current access resolver, and request bounds.
295
- * @param defaults - adapter-level thinking defaults.
296
- * @returns the fully materialized DeepSeek request body.
297
- */
298
- async function serializeRequestWithImages(options, images, defaults = {}) {
299
- assertSupportedImageRoles(options.messages);
300
- const requestMessages = offloadRequestImagesWithPolicy(options.messages, {
301
- representation: images.representation.kind === "file" ? "raw" : "base64",
302
- byteLength: (ref) => {
303
- const version = images.requestImages.get(ref.attachmentId);
304
- if (version === void 0) throw new LlmError(`DeepSeek request image ${ref.attachmentId} was not prepared.`, "INVALID_REQUEST");
305
- return version.bytes;
306
- },
307
- maxBytes: images.maxRequestImageBytes,
308
- ...images.maxImagesPerRequest === void 0 ? {} : { maxImages: images.maxImagesPerRequest },
309
- ...images.byteQuantum === void 0 ? {} : { byteQuantum: images.byteQuantum },
310
- ...images.countQuantum === void 0 ? {} : { countQuantum: images.countQuantum },
311
- placeholder: (ref) => offloadedImageText(ref, images.resolveImageAccess?.(ref))
312
- });
313
- const messages = [];
314
- if (options.system !== void 0) messages.push({
315
- role: "system",
316
- content: options.system
317
- });
318
- messages.push(...await serializeMessagesWithImages(requestMessages, images));
319
- return requestWithMessages(options, messages, defaults);
320
- }
321
- //#endregion
322
- //#region lib/types/image-tokens.js
323
- /**
324
- * DeepSeek v4 vision-token accounting: the provider's published image-token
325
- * calculator (api-docs.deepseek.com, Token & Token Usage) ported verbatim.
326
- * The provider resizes every request image onto a 14px-patch grid, downsamples
327
- * 3:1 per axis, and caps one image at 384 tokens; the port prices the
328
- * pad-to-4 alignment at its 3-token upper bound because request pricing has
329
- * no preceding-token position. Actual usage remains authoritative.
330
- *
331
- * @module dsh-llm-deepseek/image-tokens
332
- */
333
- /** Vision patch edge in pixels. */
334
- const PATCH_SIZE = 14;
335
- /** Per-axis patch-to-token downsampling ratio. */
336
- const DOWNSAMPLE_RATIO = 3;
337
- /** Provider cap on tokens for one request image. */
338
- const MAX_IMAGE_TOKENS = 384;
339
- /** Token-alignment quantum; pricing charges its worst-case `QUANTUM - 1` pad. */
340
- const COMPRESS_PAD_TO = 4;
341
- /** Width is clamped to this multiple of height before grid projection. */
342
- const MAX_WIDTH_HEIGHT_RATIO = 8;
343
- /** Total-pixel floor; smaller images are scaled up before grid projection. */
344
- const MIN_PIXELS = 384 * 384;
345
- const intDiv = (value, divisor) => Math.floor(value / divisor);
346
- const ceilDiv = (value, divisor) => Math.floor((value + divisor - 1) / divisor);
347
- /** Token count of one grid, including row separators and framing. */
348
- function gridTokens(gridHeight, gridWidth) {
349
- let tokens = gridHeight * (gridWidth + 1) + 2;
350
- if (gridHeight % 2 === 1) tokens += gridWidth + 1;
351
- tokens += ceilDiv(gridHeight, 2) * (gridWidth + 1) % 2 * 2;
352
- return tokens;
353
- }
354
- /** Solve the largest grid within `budget` tokens preserving the aspect ratio. */
355
- function solveResizeRatio(height, width, budget) {
356
- const aspect = height / width;
357
- const idealGridWidth = Math.sqrt((budget - 2) / aspect + .25) - .5;
358
- const idealGridHeight = idealGridWidth * aspect;
359
- let bestHeight;
360
- let bestWidth;
361
- if (idealGridWidth < 1) {
362
- const solvedGridWidth = 1;
363
- let solvedGridHeight = intDiv(budget - 2, 2);
364
- // v8 ignore: at the provider budget the one-column solve always lands on
365
- /* v8 ignore next */
366
- if (solvedGridHeight % 2 === 1) solvedGridHeight -= 1;
367
- bestWidth = solvedGridWidth * PATCH_SIZE * DOWNSAMPLE_RATIO;
368
- bestHeight = solvedGridHeight * PATCH_SIZE * DOWNSAMPLE_RATIO;
369
- } else if (idealGridHeight < 2) {
370
- const solvedGridHeight = 2;
371
- const solvedGridWidth = intDiv(budget - 2, solvedGridHeight) - 1;
372
- if (!(solvedGridWidth > 1)) throw new Error("deepseek image tokens: no grid fits the token budget");
373
- bestWidth = solvedGridWidth * PATCH_SIZE * DOWNSAMPLE_RATIO;
374
- bestHeight = solvedGridHeight * PATCH_SIZE * DOWNSAMPLE_RATIO;
375
- } else {
376
- const solvedGridWidth = Math.trunc(idealGridWidth);
377
- let solvedGridHeight = Math.trunc(idealGridHeight);
378
- if (solvedGridHeight % 2 === 1) solvedGridHeight -= 1;
379
- const widthScale = solvedGridWidth * PATCH_SIZE * DOWNSAMPLE_RATIO / width;
380
- const heightScale = solvedGridHeight * PATCH_SIZE * DOWNSAMPLE_RATIO / height;
381
- const scale = Math.min(widthScale, heightScale);
382
- bestWidth = Math.trunc(width * scale / PATCH_SIZE) * PATCH_SIZE;
383
- bestHeight = Math.trunc(height * scale / PATCH_SIZE) * PATCH_SIZE;
384
- }
385
- const gridHeight = ceilDiv(intDiv(bestHeight, PATCH_SIZE), DOWNSAMPLE_RATIO);
386
- const gridWidth = ceilDiv(intDiv(bestWidth, PATCH_SIZE), DOWNSAMPLE_RATIO);
387
- return {
388
- gridHeight,
389
- gridWidth,
390
- bestHeight,
391
- bestWidth,
392
- numTokens: gridTokens(gridHeight, gridWidth)
393
- };
394
- }
395
- /** Project padded pixel dimensions onto the largest in-budget token grid. */
396
- function safeResize(height, width, paddedHeight, paddedWidth) {
397
- const gridHeight = ceilDiv(intDiv(paddedHeight, PATCH_SIZE), DOWNSAMPLE_RATIO);
398
- const gridWidth = ceilDiv(intDiv(paddedWidth, PATCH_SIZE), DOWNSAMPLE_RATIO);
399
- const pad = COMPRESS_PAD_TO - 1;
400
- const budget = MAX_IMAGE_TOKENS - pad;
401
- let result = {
402
- gridHeight,
403
- gridWidth,
404
- bestHeight: paddedHeight,
405
- bestWidth: paddedWidth,
406
- numTokens: gridTokens(gridHeight, gridWidth)
407
- };
408
- if (result.numTokens > budget) {
409
- result = solveResizeRatio(height, width, budget);
410
- /* v8 ignore next 4 -- the published solver's safety net; the closed-form
411
- solve stays within budget for every geometry the clamps admit. */
412
- for (let reduced = budget; result.numTokens > budget; reduced -= 1) result = solveResizeRatio(height, width, reduced);
413
- }
414
- return {
415
- ...result,
416
- numTokens: result.numTokens + pad
417
- };
418
- }
419
- /** One clamp-scale-pad-project pass; the caller iterates it to a fixpoint. */
420
- function resizeOnce(width, height) {
421
- let clampedWidth = width;
422
- let clampedHeight = height;
423
- if (clampedWidth > clampedHeight * MAX_WIDTH_HEIGHT_RATIO) clampedWidth = clampedHeight * MAX_WIDTH_HEIGHT_RATIO;
424
- const pixels = clampedWidth * clampedHeight;
425
- if (pixels < MIN_PIXELS && pixels > 0) {
426
- const scale = Math.sqrt(MIN_PIXELS / pixels);
427
- clampedWidth = Math.trunc(clampedWidth * scale);
428
- clampedHeight = Math.trunc(clampedHeight * scale);
429
- }
430
- const paddedWidth = ceilDiv(clampedWidth, PATCH_SIZE) * PATCH_SIZE;
431
- const paddedHeight = ceilDiv(clampedHeight, PATCH_SIZE) * PATCH_SIZE;
432
- return safeResize(clampedHeight, clampedWidth, paddedHeight, paddedWidth);
433
- }
434
- function sameResize(a, b) {
435
- return a.gridHeight === b.gridHeight && a.gridWidth === b.gridWidth && a.bestHeight === b.bestHeight && a.bestWidth === b.bestWidth && a.numTokens === b.numTokens;
436
- }
437
- /**
438
- * Vision tokens DeepSeek v4 charges for one request image of the given
439
- * dimensions, at the worst-case alignment pad.
440
- * @param width - positive integer request-image width in pixels.
441
- * @param height - positive integer request-image height in pixels.
442
- * @returns the provider vision-token price, at most 384.
443
- */
444
- function deepSeekImageTokens(width, height) {
445
- let result = resizeOnce(width, height);
446
- for (let iteration = 1; iteration < 10; iteration += 1) {
447
- const next = resizeOnce(result.bestWidth, result.bestHeight);
448
- if (sameResize(next, result)) return result.numTokens;
449
- result = next;
24
+ import z from "@deepseek-ai/schemastery";
25
+ import { isVolatile } from "@deepseek-ai/cosmokit";
26
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
27
+ //#region lib/types/model-info.js
28
+ /** Protocol-independent model capabilities and reasoning choices. */
29
+ const OFF_REASONING_EFFORT = ReasoningEffortId("off");
30
+ const LOW_REASONING_EFFORT = ReasoningEffortId("low");
31
+ const HIGH_REASONING_EFFORT = ReasoningEffortId("high");
32
+ const MAX_REASONING_EFFORT = ReasoningEffortId("max");
33
+ const REASONING_EFFORTS = [
34
+ { id: ReasoningEffortId("ultra"), name: "Ultra", description: "DSCODE: max reasoning plus deliberate subagent collaboration; higher total token use." },
35
+ {
36
+ id: OFF_REASONING_EFFORT,
37
+ name: "Off",
38
+ description: "Use for simple tasks that do not need reasoning."
39
+ },
40
+ {
41
+ id: LOW_REASONING_EFFORT,
42
+ name: "Low",
43
+ description: "Prefer for routine or latency-sensitive tasks."
44
+ },
45
+ {
46
+ id: HIGH_REASONING_EFFORT,
47
+ name: "High",
48
+ description: "The default balance for most tasks."
49
+ },
50
+ {
51
+ id: MAX_REASONING_EFFORT,
52
+ name: "Max",
53
+ description: "Reserve for the hardest quality-first tasks."
450
54
  }
451
- /* v8 ignore next 2 -- the published solver's non-convergence guard; every
452
- pass is a projection, so a second identical pass is a fixpoint. */
453
- throw new Error(`deepseek image tokens: resize did not converge for ${width}x${height}`);
454
- }
455
- //#endregion
456
- //#region lib/types/request-pricing.js
457
- /**
458
- * Provider-side request-image pricing for DeepSeek routes: reproduces the
459
- * adapter's deterministic request projection (per-model pixel budget,
460
- * oldest-first offload under the raw-byte and count budgets) and prices every
461
- * retained image with the published v4 vision-token accounting. Consumed
462
- * synchronously by the token meter through `LlmAdapter.imageRequestPricing`;
463
- * provider usage remains the authoritative anchor for completed requests.
464
- *
465
- * @module dsh-llm-deepseek/request-pricing
466
- */
467
- /** Default bound on accumulated file-referenced image bytes per request. */
468
- const DEFAULT_MAX_REQUEST_FILES_BYTES = 128 * 1024 * 1024;
469
- /** Provider request image-count limit. */
470
- const DEFAULT_MAX_IMAGES_PER_REQUEST = 600;
471
- /** Total-pixel budget matching DeepSeek's normal vision projection. */
472
- const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 64e4;
473
- /** Total-pixel budget matching provider low-detail image input. */
474
- const DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET = 512 * 512;
475
- /** Encoded-byte target for one deterministic model-request image; the smallest quality-ladder output is used when no quality fits. */
476
- const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
477
- /**
478
- * Resolve the request-image budgets owned by one DeepSeek model route.
479
- * @param model - Advertised model route and its optional image overrides.
480
- * @returns Complete pixel and encoded-byte budgets.
481
- * @internal
55
+ ];
56
+ const OFF_ONLY_REASONING_EFFORTS = [{
57
+ id: OFF_REASONING_EFFORT,
58
+ name: "Off",
59
+ description: "Use for simple tasks that do not need reasoning."
60
+ }];
61
+ /** Advertise one catalog entry.
62
+ * @param provider - registered provider id.
63
+ * @param model - advisory catalog entry.
64
+ * @returns selector metadata.
482
65
  */
483
- function resolveRequestImagePolicy(model) {
66
+ function catalogModelInfo(provider, model) {
484
67
  return {
485
- maxPixels: model.imagePixelBudget === "low" ? DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET : model.imagePixelBudget ?? 64e4,
486
- maxBytes: model.imageMaxBytes === void 0 ? DEFAULT_REQUEST_IMAGE_MAX_BYTES : model.imageMaxBytes
68
+ provider,
69
+ id: model.id,
70
+ name: model.name ?? model.id,
71
+ ...model.description === void 0 ? {} : { description: model.description },
72
+ inputModalities: model.inputModalities ?? ["text"]
487
73
  };
488
74
  }
489
- /**
490
- * Price one occurrence a text-only route substitutes with deterministic text,
491
- * reproducing the `projectImagesForTextModel` substitution `LlmRuntime`
492
- * applies before dispatching to a route without the `image` modality.
75
+ /** Resolve model capabilities against one configuration generation.
76
+ * @param connection - validated connection facts.
77
+ * @param provider - registered provider id.
78
+ * @param model - requested wire model id.
79
+ * @returns effective model metadata for this operation.
493
80
  */
494
- function textOnlyPrice(ref) {
81
+ function modelInfo(connection, provider, model) {
82
+ const configured = connection.models.find((entry) => entry.id === model);
83
+ const contextWindow = configured?.contextWindow ?? connection.defaultContextWindow;
495
84
  return {
496
- visualTokens: 0,
497
- text: textOnlyImageText(ref)
85
+ ...configured === void 0 ? {
86
+ provider,
87
+ id: model,
88
+ name: model,
89
+ inputModalities: ["text"]
90
+ } : catalogModelInfo(provider, configured),
91
+ context: { contextWindow },
92
+ defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens,
93
+ ...configured?.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: configured.systemPromptUpdate },
94
+ ...connection.defaults.thinking === "disabled" ? { reasoning: {
95
+ efforts: OFF_ONLY_REASONING_EFFORTS,
96
+ defaultEffort: OFF_REASONING_EFFORT
97
+ } } : { reasoning: {
98
+ efforts: REASONING_EFFORTS,
99
+ 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
100
+ } }
498
101
  };
499
102
  }
500
- /**
501
- * Build the request-image pricing for one DeepSeek route from a validated
502
- * connection snapshot. Uncatalogued and text-only models price every
503
- * occurrence as its deterministic text substitution; image-capable models
504
- * reproduce the adapter's first-stage oldest-first offload from durable byte
505
- * lengths and price retained images by their projected request dimensions,
506
- * with each occurrence's handle or placeholder text built through the same
507
- * access resolution the serializer uses. The base64 fallback's tighter inline
508
- * budget is not reproduced, so a fallback request can only cost less than
509
- * this estimate; access paths resolve at pricing time, so a path that changes
510
- * before the request only shifts the text price by its own length.
511
- * @param connection - validated connection facts of the pricing resolution.
512
- * @param model - exact model id named by the request header.
513
- * @param resolveAccess - current execution-world access resolution shared with request serialization.
514
- * @returns synchronous per-occurrence pricing for the route.
515
- */
516
- function deepSeekImageRequestPricing(connection, model, resolveAccess) {
517
- const catalogModel = connection.models.find((entry) => entry.id === model);
518
- if (catalogModel?.inputModalities?.includes("image") !== true) return { priceImages: (images) => images.map(textOnlyPrice) };
519
- const policy = resolveRequestImagePolicy(catalogModel);
520
- return { priceImages: (images) => {
521
- const offloaded = offloadedImagePrefixCount(images.map((ref) => Math.min(ref.bytes, policy.maxBytes)), {
522
- maxBytes: connection.maxRequestFilesBytes,
523
- maxImages: connection.maxImagesPerRequest,
524
- byteQuantum: connection.imageOffloadByteQuantum,
525
- countQuantum: connection.imageOffloadCountQuantum
526
- });
527
- return images.map((ref, index) => {
528
- if (index < offloaded) return {
529
- visualTokens: 0,
530
- text: offloadedImageText(ref, resolveAccess?.(ref))
531
- };
532
- const dimensions = requestImageDimensions(ref.width, ref.height, policy.maxPixels);
533
- return {
534
- visualTokens: deepSeekImageTokens(dimensions.width, dimensions.height),
535
- text: requestImageHandleText(ref, dimensions, resolveAccess?.(ref))
536
- };
537
- });
538
- } };
539
- }
540
103
  //#endregion
541
104
  //#region lib/types/file-id.js
542
105
  /** DeepSeek Files API identifiers. @module dsh-llm-deepseek/file-id */
@@ -557,8 +120,22 @@ function DeepSeekFileScope(scope) {
557
120
  return scope;
558
121
  }
559
122
  //#endregion
123
+ //#region lib/types/messages-api.js
124
+ /** Shared DeepSeek Messages API endpoint and header policy. @module dsh-llm-deepseek/messages-api */
125
+ /** Required opt-in for Messages file operations and file-referenced image requests. */
126
+ const MESSAGES_FILES_BETA = "files-api-2025-04-14";
127
+ /**
128
+ * Resolve the API root without duplicating an explicit provider version path.
129
+ * @param baseURL - validated configured endpoint root.
130
+ * @returns the root beneath which Messages resources are exposed.
131
+ */
132
+ function messagesApiRoot(baseURL) {
133
+ const base = baseURL.replace(/\/+$/u, "");
134
+ return new URL(base).pathname.endsWith("/v1") ? base : `${base}/v1`;
135
+ }
136
+ //#endregion
560
137
  //#region lib/types/files-api.js
561
- /** OpenAI-compatible DeepSeek Files API transport. @module dsh-llm-deepseek/files-api */
138
+ /** DeepSeek Files API transport. @module dsh-llm-deepseek/files-api */
562
139
  /** Minimum provider-supported file lifetime. */
563
140
  const MIN_FILE_EXPIRY_SECONDS = 3600;
564
141
  /** Maximum provider-supported file lifetime. */
@@ -595,20 +172,31 @@ function isFilesQuotaError(error) {
595
172
  function invalidResponse(operation) {
596
173
  return new LlmError(`DeepSeek Files API returned an invalid ${operation} response.`, "INVALID_RESPONSE");
597
174
  }
175
+ /** Decode successful Files JSON with operation context; body transport and abort failures retain their identity. */
176
+ async function responseJson(response, operation) {
177
+ try {
178
+ return await response.json();
179
+ } catch (error) {
180
+ if (!(error instanceof SyntaxError)) throw error;
181
+ throw new LlmError(`DeepSeek Files API returned invalid JSON for ${operation} (HTTP ${response.status}).`, "INVALID_RESPONSE", {
182
+ status: response.status,
183
+ cause: error
184
+ });
185
+ }
186
+ }
598
187
  function parseFileObject(value, operation) {
599
188
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidResponse(operation);
600
189
  const wire = value;
601
- if (typeof wire.id !== "string" || wire.id.length === 0 || wire.object !== "file" || !Number.isSafeInteger(wire.bytes) || wire.bytes < 0 || !Number.isSafeInteger(wire.created_at) || wire.created_at < 0 || typeof wire.filename !== "string" || wire.filename.length === 0 || wire.purpose !== "user_data" || wire.expires_at !== void 0 && (!Number.isSafeInteger(wire.expires_at) || wire.expires_at < 0)) throw invalidResponse(operation);
190
+ const createdAt = typeof wire.created_at === "string" ? Math.floor(Date.parse(wire.created_at) / 1e3) : NaN;
191
+ 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);
602
192
  return {
603
193
  id: DeepSeekFileId(wire.id),
604
- bytes: wire.bytes,
605
- createdAt: wire.created_at,
606
- filename: wire.filename,
607
- purpose: "user_data",
608
- ...wire.expires_at === void 0 ? {} : { expiresAt: wire.expires_at }
194
+ bytes: wire.size_bytes,
195
+ createdAt,
196
+ filename: wire.filename
609
197
  };
610
198
  }
611
- function providerErrorDetail(value) {
199
+ function providerErrorDetail$1(value) {
612
200
  if (value === null || typeof value !== "object" || Array.isArray(value)) return { detail: "" };
613
201
  const error = value.error;
614
202
  if (error === null || typeof error !== "object" || Array.isArray(error)) return { detail: "" };
@@ -623,26 +211,31 @@ function providerErrorDetail(value) {
623
211
  ].filter((field) => typeof field === "string").join(" ")
624
212
  };
625
213
  }
626
- /** Direct client for the OpenAI-compatible `/files` endpoints. */
214
+ /** Direct Files client retaining the configured URL root and refusing redirects before credentials can leave its origin. */
627
215
  var DeepSeekFilesClient = class {
628
216
  baseURL;
217
+ accountCredential;
629
218
  apiKey;
630
219
  fetchImpl;
631
220
  /**
632
221
  * @param options - endpoint, API-key snapshot, and optional test transport.
633
222
  */
634
223
  constructor(options) {
635
- this.baseURL = options.baseURL.replace(/\/+$/u, "");
636
224
  this.apiKey = options.apiKey;
225
+ this.accountCredential = options.accountCredential === true;
637
226
  this.fetchImpl = options.fetch ?? globalThis.fetch;
227
+ this.baseURL = messagesApiRoot(options.baseURL);
638
228
  }
639
229
  async request(path, init, signal) {
640
230
  let response;
641
231
  try {
642
232
  const headers = new Headers(attributionHeaders());
643
- headers.set("authorization", `Bearer ${this.apiKey}`);
233
+ headers.set(this.accountCredential ? "x-dsh-auth-token" : "x-api-key", this.apiKey);
234
+ headers.set("anthropic-version", "2023-06-01");
235
+ headers.set("anthropic-beta", MESSAGES_FILES_BETA);
644
236
  response = await this.fetchImpl(`${this.baseURL}${path}`, {
645
237
  ...init,
238
+ redirect: "error",
646
239
  headers,
647
240
  ...signal === void 0 ? {} : { signal }
648
241
  });
@@ -655,50 +248,50 @@ var DeepSeekFilesClient = class {
655
248
  try {
656
249
  parsed = await response.json();
657
250
  } catch {}
658
- const { message, detail } = providerErrorDetail(parsed);
251
+ const { message, detail } = providerErrorDetail$1(parsed);
659
252
  throw new DeepSeekFilesError(message ?? `DeepSeek Files API error (HTTP ${response.status})`, response.status, detail);
660
253
  }
661
254
  /**
662
255
  * Upload one image with an explicit expiry.
663
256
  * @param input - deterministic request-version bytes, media type, filename, lifetime, and cancellation.
664
- * @returns the validated provider file object, including `expires_at`.
257
+ * @returns the validated file and reuse deadline. Messages omits expiry metadata;
258
+ * its deadline uses upload creation plus the requested lifetime.
665
259
  */
666
260
  async upload(input) {
667
261
  if (input.data.byteLength > 134217728) throw new LlmError("DeepSeek Files API upload exceeds 128 MiB.", "INVALID_REQUEST");
668
262
  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");
669
263
  const form = new FormData();
670
- form.set("purpose", "user_data");
671
264
  form.set("expires_after[anchor]", "created_at");
672
265
  form.set("expires_after[seconds]", String(input.expiresAfterSeconds));
673
266
  form.set("file", new Blob([Uint8Array.from(input.data).buffer], { type: input.mediaType }), input.filename);
674
- const file = parseFileObject(await (await this.request("/files", {
267
+ const file = parseFileObject(await responseJson(await this.request("/files", {
675
268
  method: "POST",
676
269
  body: form
677
- }, input.signal)).json(), "upload");
678
- if (file.expiresAt === void 0) throw invalidResponse("upload");
270
+ }, input.signal), "upload"), "upload");
679
271
  return {
680
272
  ...file,
681
- expiresAt: file.expiresAt
273
+ expiresAt: file.createdAt + input.expiresAfterSeconds
682
274
  };
683
275
  }
684
276
  /**
685
- * List one ascending or descending page of user-data files.
686
- * @param options - pagination, ordering, and cancellation.
687
- * @returns the validated page.
277
+ * List one provider-ordered page of files.
278
+ * @param options - pagination and cancellation.
279
+ * @returns the validated page with null cursors omitted.
688
280
  */
689
281
  async list(options = {}) {
690
- const query = new URLSearchParams({ purpose: "user_data" });
691
- if (options.after !== void 0) query.set("after", options.after);
282
+ const query = new URLSearchParams();
283
+ if (options.after !== void 0) query.set("after_id", options.after);
692
284
  if (options.limit !== void 0) query.set("limit", String(options.limit));
693
- if (options.order !== void 0) query.set("order", options.order);
694
- const value = await (await this.request(`/files?${query.toString()}`, { method: "GET" }, options.signal)).json();
285
+ const value = await responseJson(await this.request(`/files?${query.toString()}`, { method: "GET" }, options.signal), "list");
695
286
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidResponse("list");
696
287
  const wire = value;
697
- if (wire.object !== "list" || !Array.isArray(wire.data) || typeof wire.has_more !== "boolean" || wire.first_id !== void 0 && typeof wire.first_id !== "string" || wire.last_id !== void 0 && typeof wire.last_id !== "string") throw invalidResponse("list");
288
+ const firstId = wire.first_id ?? void 0;
289
+ const lastId = wire.last_id ?? void 0;
290
+ 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");
698
291
  return {
699
292
  data: wire.data.map((item) => parseFileObject(item, "list")),
700
- ...typeof wire.first_id === "string" ? { firstId: DeepSeekFileId(wire.first_id) } : {},
701
- ...typeof wire.last_id === "string" ? { lastId: DeepSeekFileId(wire.last_id) } : {},
293
+ ...typeof firstId === "string" ? { firstId: DeepSeekFileId(firstId) } : {},
294
+ ...typeof lastId === "string" ? { lastId: DeepSeekFileId(lastId) } : {},
702
295
  hasMore: wire.has_more
703
296
  };
704
297
  }
@@ -709,7 +302,7 @@ var DeepSeekFilesClient = class {
709
302
  * @returns the validated file object.
710
303
  */
711
304
  async retrieve(fileId, signal) {
712
- return parseFileObject(await (await this.request(`/files/${encodeURIComponent(fileId)}`, { method: "GET" }, signal)).json(), "retrieve");
305
+ return parseFileObject(await responseJson(await this.request(`/files/${encodeURIComponent(fileId)}`, { method: "GET" }, signal), "retrieve"), "retrieve");
713
306
  }
714
307
  /**
715
308
  * Delete one provider file.
@@ -717,10 +310,10 @@ var DeepSeekFilesClient = class {
717
310
  * @param signal - request cancellation.
718
311
  */
719
312
  async delete(fileId, signal) {
720
- const value = await (await this.request(`/files/${encodeURIComponent(fileId)}`, { method: "DELETE" }, signal)).json();
313
+ const value = await responseJson(await this.request(`/files/${encodeURIComponent(fileId)}`, { method: "DELETE" }, signal), "delete");
721
314
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidResponse("delete");
722
315
  const wire = value;
723
- if (wire.id !== fileId || wire.object !== "file" || wire.deleted !== true) throw invalidResponse("delete");
316
+ if (wire.id !== fileId || wire.type !== "file_deleted") throw invalidResponse("delete");
724
317
  }
725
318
  };
726
319
  //#endregion
@@ -890,9 +483,13 @@ var DeepSeekUploadIndex = class {
890
483
  //#endregion
891
484
  //#region lib/types/file-store.js
892
485
  /** DeepSeek Files API upload reuse, invalidation, and quota recovery. @module dsh-llm-deepseek/file-store */
893
- /** DeepSeek chat accepts at most 32 MiB per image even when it is referenced by file id. */
894
- const MAX_CHAT_IMAGE_BYTES = 32 * 1024 * 1024;
486
+ /** Shared Files-store limit for each request image, including file-id references. */
487
+ const MAX_IMAGE_BYTES = 32 * 1024 * 1024;
895
488
  const OWNED_FILE_PREFIX = "dsh-";
489
+ /** The Files resource's parent URL identifies the upload namespace. */
490
+ function fileScope(connection) {
491
+ return deepSeekFileScope(messagesApiRoot(connection.baseURL), connection.apiKey);
492
+ }
896
493
  function abortReason(signal) {
897
494
  const reason = signal.reason;
898
495
  return reason instanceof Error ? reason : new Error("DeepSeek file upload cancelled with a non-Error reason.", { cause: reason });
@@ -960,6 +557,7 @@ var DeepSeekFileStore = class {
960
557
  return new DeepSeekFilesClient({
961
558
  baseURL: connection.baseURL,
962
559
  apiKey: connection.apiKey,
560
+ ...connection.accountCredential === void 0 ? {} : { accountCredential: connection.accountCredential },
963
561
  ...this.fetchImpl === void 0 ? {} : { fetch: this.fetchImpl }
964
562
  });
965
563
  }
@@ -973,7 +571,7 @@ var DeepSeekFileStore = class {
973
571
  */
974
572
  ensureUploaded(version, connection, policy, signal) {
975
573
  signal?.throwIfAborted();
976
- const key = `${deepSeekFileScope(connection.baseURL, connection.apiKey)}\0${version.variantId}`;
574
+ const key = `${fileScope(connection)}\0${version.variantId}`;
977
575
  let active = this.inflight.get(key);
978
576
  if (active?.controller.signal.aborted) {
979
577
  this.inflight.delete(key);
@@ -1001,8 +599,8 @@ var DeepSeekFileStore = class {
1001
599
  return waitForUpload(shared, signal);
1002
600
  }
1003
601
  async ensureUploadedOnce(version, connection, policy, signal) {
1004
- if (version.bytes > 33554432) throw new LlmError("DeepSeek chat image exceeds the 32 MiB per-image limit.", "INVALID_REQUEST");
1005
- const scope = deepSeekFileScope(connection.baseURL, connection.apiKey);
602
+ if (version.bytes > 33554432) throw new LlmError("DeepSeek image exceeds the 32 MiB per-image limit.", "INVALID_REQUEST");
603
+ const scope = fileScope(connection);
1006
604
  const now = this.now();
1007
605
  const marginMs = policy.refreshMarginSeconds * 1e3;
1008
606
  const cached = await this.index.get(scope, version.variantId, now, marginMs);
@@ -1048,13 +646,13 @@ var DeepSeekFileStore = class {
1048
646
  };
1049
647
  }
1050
648
  /**
1051
- * Invalidate one exact local mapping after the chat endpoint rejects its remote id.
649
+ * Invalidate one exact local mapping after a model request rejects its remote id.
1052
650
  * @param version - request-image version whose remote generation failed.
1053
651
  * @param fileId - exact rejected file id.
1054
652
  * @param connection - endpoint and API-key snapshot.
1055
653
  */
1056
654
  async invalidate(version, fileId, connection) {
1057
- await this.index.remove(deepSeekFileScope(connection.baseURL, connection.apiKey), version.variantId, fileId);
655
+ await this.index.remove(fileScope(connection), version.variantId, fileId);
1058
656
  }
1059
657
  /**
1060
658
  * Delete the indexed remote file for one attachment and remove its local mapping.
@@ -1065,7 +663,7 @@ var DeepSeekFileStore = class {
1065
663
  * @returns whether an indexed file existed and was deleted.
1066
664
  */
1067
665
  async release(version, connection, policy, signal) {
1068
- const scope = deepSeekFileScope(connection.baseURL, connection.apiKey);
666
+ const scope = fileScope(connection);
1069
667
  const record = await this.index.get(scope, version.variantId, this.now(), policy.refreshMarginSeconds * 1e3);
1070
668
  if (record === void 0) return false;
1071
669
  await this.client(connection).delete(record.fileId, signal);
@@ -1083,22 +681,25 @@ var DeepSeekFileStore = class {
1083
681
  const client = this.client(connection);
1084
682
  let after;
1085
683
  const owned = [];
1086
- while (owned.length < count) {
684
+ while (true) {
1087
685
  const page = await client.list({
1088
686
  ...after === void 0 ? {} : { after },
1089
687
  limit: 1e3,
1090
- order: "asc",
1091
688
  ...signal === void 0 ? {} : { signal }
1092
689
  });
1093
690
  for (const file of page.data) {
1094
691
  if (!file.filename.startsWith(OWNED_FILE_PREFIX)) continue;
1095
- owned.push(file.id);
1096
- if (owned.length === count) break;
692
+ owned.push({
693
+ id: file.id,
694
+ createdAt: file.createdAt
695
+ });
1097
696
  }
697
+ owned.sort((left, right) => left.createdAt - right.createdAt);
698
+ owned.splice(count);
1098
699
  if (!page.hasMore || page.lastId === void 0 || page.lastId === after) break;
1099
700
  after = page.lastId;
1100
701
  }
1101
- for (const fileId of owned) await client.delete(fileId, signal);
702
+ for (const file of owned) await client.delete(file.id, signal);
1102
703
  return owned.length;
1103
704
  }
1104
705
  /**
@@ -1114,242 +715,1058 @@ var DeepSeekFileStore = class {
1114
715
  total += deleted;
1115
716
  if (deleted < 1e3) break;
1116
717
  }
1117
- await this.index.clear(deepSeekFileScope(connection.baseURL, connection.apiKey));
718
+ await this.index.clear(fileScope(connection));
1118
719
  return total;
1119
720
  }
1120
721
  };
1121
- /**
1122
- * Parse an SSE byte stream into data payloads. Yields `[DONE]` as the final
1123
- * value and returns; throws `LlmError('STREAM_CLOSED')` when the stream ends
1124
- * without it (truncated response — the model call cannot be trusted).
1125
- * @param stream - raw SSE bytes; reads may split anywhere, including mid-UTF-8 sequence.
1126
- * @param onComment - optional transport-activity callback; comments never enter the yielded payload stream.
1127
- * @returns each event's data payload in arrival order, the `[DONE]` sentinel last.
1128
- */
1129
- async function* parseSse(stream, onComment) {
1130
- const events = stream.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream({ onComment }));
1131
- for await (const { data } of events) {
1132
- yield data;
1133
- if (data === "[DONE]") return;
1134
- }
1135
- throw new LlmError("SSE stream ended without [DONE]", "STREAM_CLOSED");
1136
- }
1137
722
  //#endregion
1138
- //#region lib/types/translate.js
1139
- /**
1140
- * Translate DeepSeek SSE payloads with one stateful harness block per content, reasoning, or tool
1141
- * call index. An empty initial reasoning delta does not open a block. Finish reason and the latest
1142
- * usage are deferred until `[DONE]`, covering both finish-attached and trailing usage-only shapes
1143
- * while ensuring no chunk follows `finish`.
1144
- *
1145
- * Translate DeepSeek wire chunks into the harness `StreamChunk` protocol.
1146
- * @module dsh-llm-deepseek/translate
1147
- */
1148
- /**
1149
- * Map the wire finish_reason vocabulary to the harness FinishReason.
1150
- * @param reason - the wire `finish_reason` string.
1151
- * @returns the mapped reason; unrecognized values (content_filter, …) become `{kind: 'error'}` with the uppercased value as `code`.
1152
- */
1153
- function mapFinishReason(reason) {
1154
- switch (reason) {
1155
- case "stop": return { kind: "stop" };
1156
- case "tool_calls": return { kind: "tool-calls" };
1157
- case "length": return { kind: "max-tokens" };
1158
- default: return {
1159
- kind: "error",
1160
- failure: {
1161
- message: `model stopped: ${reason}`,
1162
- code: reason.toUpperCase()
1163
- }
1164
- };
1165
- }
1166
- }
1167
- /**
1168
- * Map wire usage fields. DeepSeek's `prompt_tokens` INCLUDES cache hits
1169
- * (`prompt_tokens = prompt_cache_hit_tokens + prompt_cache_miss_tokens`,
1170
- * api/create-chat-completion); the harness TokenUsage convention is
1171
- * DISJOINT counts, so cache reads are subtracted out of `inputTokens`.
1172
- * @param usage - wire usage from the finish chunk or the trailing usage-only chunk.
1173
- * @returns disjoint harness counts; an exact total is present only when the
1174
- * aggregate prompt/completion counters are valid and agree with any wire total.
1175
- */
1176
- function mapUsage(usage) {
1177
- const cacheRead = usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens;
1178
- const reasoning = usage.completion_tokens_details?.reasoning_tokens;
1179
- const combined = usage.prompt_tokens + usage.completion_tokens;
1180
- const hasExactTotal = Number.isSafeInteger(usage.prompt_tokens) && usage.prompt_tokens >= 0 && Number.isSafeInteger(usage.completion_tokens) && usage.completion_tokens >= 0 && Number.isSafeInteger(combined) && (usage.total_tokens === void 0 || usage.total_tokens === combined);
1181
- return {
1182
- inputTokens: usage.prompt_tokens - (cacheRead ?? 0),
1183
- outputTokens: usage.completion_tokens,
1184
- ...hasExactTotal ? { totalTokens: combined } : {},
1185
- ...cacheRead !== void 0 ? { cacheReadTokens: cacheRead } : {},
1186
- ...reasoning !== void 0 ? { reasoningTokens: reasoning } : {}
723
+ //#region lib/types/request-files.js
724
+ /** Shared Files resolution, bounded stale-id recovery, and normalized-image diagnostics. */
725
+ var __addDisposableResource$1 = function(env, value, async) {
726
+ if (value !== null && value !== void 0) {
727
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
728
+ var dispose, inner;
729
+ if (async) {
730
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
731
+ dispose = value[Symbol.asyncDispose];
732
+ }
733
+ if (dispose === void 0) {
734
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
735
+ dispose = value[Symbol.dispose];
736
+ if (async) inner = dispose;
737
+ }
738
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
739
+ if (inner) dispose = function() {
740
+ try {
741
+ inner.call(this);
742
+ } catch (e) {
743
+ return Promise.reject(e);
744
+ }
745
+ };
746
+ env.stack.push({
747
+ value,
748
+ dispose,
749
+ async
750
+ });
751
+ } else if (async) env.stack.push({ async: true });
752
+ return value;
753
+ };
754
+ var __disposeResources$1 = (function(SuppressedError) {
755
+ return function(env) {
756
+ function fail(e) {
757
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
758
+ env.hasError = true;
759
+ }
760
+ var r, s = 0;
761
+ function next() {
762
+ while (r = env.stack.pop()) try {
763
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
764
+ if (r.dispose) {
765
+ var result = r.dispose.call(r.value);
766
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
767
+ fail(e);
768
+ return next();
769
+ });
770
+ } else s |= 1;
771
+ } catch (e) {
772
+ fail(e);
773
+ }
774
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
775
+ if (env.hasError) throw env.error;
776
+ }
777
+ return next();
778
+ };
779
+ })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
780
+ var e = new Error(message);
781
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
782
+ });
783
+ /** A file upload failure eligible for request-wide inline fallback. */
784
+ var FileResolutionFailure = class extends Error {
785
+ constructor(cause) {
786
+ super("DeepSeek Files API could not resolve a request image.", { cause });
787
+ this.name = "FileResolutionFailure";
788
+ }
789
+ };
790
+ function providerRejectedNormalizedImage(detail) {
791
+ 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);
792
+ }
793
+ function providerRejectedFileId(detail) {
794
+ const file = /\bfile(?:[_ -]?(?:id|api|not[_ -]?found|deleted|expired))?/iu.test(detail);
795
+ const missing = /(?:expired|not[_ -]?found|deleted|do(?:es)? not exist|not created under (?:this|your) account)/iu.test(detail);
796
+ const invalidId = /(?:invalid.{0,20}file[_ -]?(?:id|api)|file[_ -]?(?:id|api).{0,20}invalid)/iu.test(detail);
797
+ return file && (missing || invalidId);
798
+ }
799
+ function detailNamesFileId(detail, fileId) {
800
+ let index = detail.indexOf(fileId);
801
+ while (index >= 0) {
802
+ const before = detail[index - 1];
803
+ const after = detail[index + fileId.length];
804
+ if ((before === void 0 || !/[\p{L}\p{N}_-]/u.test(before)) && (after === void 0 || !/[\p{L}\p{N}_-]/u.test(after))) return true;
805
+ index = detail.indexOf(fileId, index + 1);
806
+ }
807
+ return false;
808
+ }
809
+ function staleMappings(files, detail) {
810
+ const unique = [...new Map(files.map((file) => [`${file.version.variantId}\0${file.fileId}`, file])).values()];
811
+ const exact = unique.filter((file) => detailNamesFileId(detail, file.fileId));
812
+ return exact.length > 0 ? exact : unique;
813
+ }
814
+ function normalizedImageFacts(file) {
815
+ const version = file.version;
816
+ const name = version.attachment.name ?? version.attachment.attachmentId;
817
+ const colour = version.hasAlpha ? "sRGBA" : "sRGB";
818
+ return `"${name}" at message ${file.location.message}, image ${file.location.image} (${version.mediaType}, 8-bit ${colour}, ${version.width}x${version.height})`;
819
+ }
820
+ function normalizedImageDiagnostic(files, providerMessage, providerDetail) {
821
+ const target = files.find((file) => detailNamesFileId(providerDetail, file.fileId)) ?? (files.length === 1 ? files[0] : void 0);
822
+ 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.`;
823
+ 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.`;
824
+ }
825
+ /** Files state owned by one model request, including at most one stale-id retry. */
826
+ var RequestFiles = class {
827
+ files;
828
+ connection;
829
+ policy;
830
+ timeoutMs;
831
+ signal;
832
+ activity;
833
+ used = [];
834
+ retried = false;
835
+ constructor(files, connection, policy, timeoutMs, signal, activity) {
836
+ this.files = files;
837
+ this.connection = connection;
838
+ this.policy = policy;
839
+ this.timeoutMs = timeoutMs;
840
+ this.signal = signal;
841
+ this.activity = activity;
842
+ }
843
+ /** Reset occurrence tracking before serializing the next HTTP attempt. */
844
+ beginAttempt() {
845
+ this.used = [];
846
+ }
847
+ /**
848
+ * Resolve a retained image under its own upload deadline.
849
+ * @param version - prepared request image.
850
+ * @param location - occurrence used by provider-rejection diagnostics.
851
+ * @returns the reusable provider id.
852
+ */
853
+ async resolve(version, location) {
854
+ const env_1 = {
855
+ stack: [],
856
+ error: void 0,
857
+ hasError: false
858
+ };
859
+ try {
860
+ const limit = __addDisposableResource$1(env_1, deadline(this.signal, this.timeoutMs, "DEEPSEEK_FILES_API_TIMEOUT"), false);
861
+ let resolved;
862
+ try {
863
+ resolved = await this.files.ensureUploaded(version, this.connection, this.policy, limit.signal);
864
+ } catch (error) {
865
+ if (this.signal.aborted) throw error;
866
+ throw new FileResolutionFailure(error);
867
+ }
868
+ this.activity();
869
+ this.used.push({
870
+ version,
871
+ fileId: resolved.record.fileId,
872
+ location
873
+ });
874
+ return resolved.record.fileId;
875
+ } catch (e_1) {
876
+ env_1.error = e_1;
877
+ env_1.hasError = true;
878
+ } finally {
879
+ __disposeResources$1(env_1);
880
+ }
881
+ }
882
+ /**
883
+ * Invalidate rejected mappings; only the first stale-id response permits another request.
884
+ * @param detail - provider error fields used for stale-id classification.
885
+ * @returns whether the caller should serialize and dispatch again.
886
+ */
887
+ async retry(detail) {
888
+ if (this.used.length === 0 || !providerRejectedFileId(detail)) return false;
889
+ await Promise.all(staleMappings(this.used, detail).map((file) => this.files.invalidate(file.version, file.fileId, this.connection)));
890
+ if (this.retried) return false;
891
+ this.retried = true;
892
+ return true;
893
+ }
894
+ /**
895
+ * Attribute a normalized-image rejection to the actual uploaded image occurrences.
896
+ * @param status - rejected request's HTTP status.
897
+ * @param message - provider's error message.
898
+ * @param detail - provider error classification fields.
899
+ * @returns the image diagnostic or the original provider message.
900
+ */
901
+ errorMessage(status, message, detail) {
902
+ return status === 400 && this.used.length > 0 && providerRejectedNormalizedImage(detail) ? normalizedImageDiagnostic(this.used, message, detail) : message;
903
+ }
904
+ };
905
+ //#endregion
906
+ //#region lib/types/request-extensions.js
907
+ /** Prepare plugin-contributed request fields and commit their delivery after HTTP acceptance. */
908
+ /**
909
+ * Merge contributions without replacing Messages fields. Preparation and
910
+ * acceptance failures report REQUEST_EXTENSION.
911
+ * @param body - serialized Messages request before extension fields.
912
+ * @param options - request identity, purpose, and cancellation.
913
+ * @param prepare - contributor registry captured for this adapter.
914
+ * @returns HTTP payload and a commit to invoke only after a successful HTTP response.
915
+ */
916
+ async function prepareRequestExtensions(body, options, prepare) {
917
+ let extensions;
918
+ try {
919
+ extensions = await prepare({
920
+ body,
921
+ ...options
922
+ });
923
+ } catch (error) {
924
+ throw new LlmError("DeepSeek request extension preparation failed", "REQUEST_EXTENSION", { cause: error });
925
+ }
926
+ for (const field of Object.keys(extensions.fields)) if (Object.hasOwn(body, field)) throw new LlmError(`DeepSeek request extension field ${JSON.stringify(field)} collides with the base request`, "REQUEST_EXTENSION");
927
+ return {
928
+ payload: JSON.stringify({
929
+ ...body,
930
+ ...extensions.fields
931
+ }),
932
+ async accept() {
933
+ try {
934
+ await extensions.accept();
935
+ } catch (error) {
936
+ throw new LlmError("DeepSeek request extension acceptance failed", "REQUEST_EXTENSION", { cause: error });
937
+ }
938
+ }
939
+ };
940
+ }
941
+ //#endregion
942
+ //#region lib/types/image-tokens.js
943
+ /**
944
+ * DeepSeek vision-token accounting: the provider's published image-token
945
+ * calculator (api-docs.deepseek.com, Token & Token Usage) ported verbatim in
946
+ * its current `v41` configuration. The provider scales an image below
947
+ * 544×544 total pixels up, aligns it to a 14px-patch grid, downsamples 3:1
948
+ * per axis into token cells, and caps one image at 1024 tokens by solving the
949
+ * largest aspect-preserving grid inside that budget. The count is exact: this
950
+ * configuration has no alignment pad and no aspect-ratio clamp. Actual usage
951
+ * remains authoritative.
952
+ *
953
+ * @module dsh-llm-deepseek/image-tokens
954
+ */
955
+ /** Vision patch edge in pixels. */
956
+ const PATCH_SIZE = 14;
957
+ /** Per-axis patch-to-token downsampling ratio. */
958
+ const DOWNSAMPLE_RATIO = 3;
959
+ /** Provider cap on tokens for one request image. */
960
+ const MAX_IMAGE_TOKENS = 1024;
961
+ /** Total-pixel floor; smaller images are scaled up before grid projection. */
962
+ const MIN_PIXELS = 544 * 544;
963
+ /** Pixels covered by one token cell along either axis. */
964
+ const CELL_SIZE = PATCH_SIZE * DOWNSAMPLE_RATIO;
965
+ const intDiv = (value, divisor) => Math.floor(value / divisor);
966
+ const ceilDiv = (value, divisor) => Math.floor((value + divisor - 1) / divisor);
967
+ /** Token count of one grid: every row carries a separator, plus two framing tokens. */
968
+ function gridTokens(gridHeight, gridWidth) {
969
+ return gridHeight * (gridWidth + 1) + 2;
970
+ }
971
+ /** Token-cell count along one padded pixel axis. */
972
+ function gridCells(paddedLength) {
973
+ return ceilDiv(intDiv(paddedLength, PATCH_SIZE), DOWNSAMPLE_RATIO);
974
+ }
975
+ /** Solve the largest grid within `budget` tokens preserving the aspect ratio. */
976
+ function solveResizeRatio(height, width, budget) {
977
+ const aspect = height / width;
978
+ const idealGridWidth = Math.sqrt((budget - 2) / aspect + .25) - .5;
979
+ const idealGridHeight = idealGridWidth * aspect;
980
+ let bestHeight;
981
+ let bestWidth;
982
+ if (idealGridWidth < 1) {
983
+ const solvedGridWidth = 1;
984
+ const solvedGridHeight = intDiv(budget - 2, 2);
985
+ bestWidth = solvedGridWidth * CELL_SIZE;
986
+ bestHeight = solvedGridHeight * CELL_SIZE;
987
+ } else if (idealGridHeight < 1) {
988
+ const solvedGridHeight = 1;
989
+ bestWidth = (intDiv(budget - 2, solvedGridHeight) - 1) * CELL_SIZE;
990
+ bestHeight = solvedGridHeight * CELL_SIZE;
991
+ } else {
992
+ const solvedGridWidth = Math.trunc(idealGridWidth);
993
+ const solvedGridHeight = Math.trunc(idealGridHeight);
994
+ const scale = Math.min(solvedGridWidth * CELL_SIZE / width, solvedGridHeight * CELL_SIZE / height);
995
+ bestWidth = Math.trunc(width * scale / PATCH_SIZE) * PATCH_SIZE;
996
+ bestHeight = Math.trunc(height * scale / PATCH_SIZE) * PATCH_SIZE;
997
+ }
998
+ const gridHeight = gridCells(bestHeight);
999
+ const gridWidth = gridCells(bestWidth);
1000
+ return {
1001
+ gridHeight,
1002
+ gridWidth,
1003
+ bestHeight,
1004
+ bestWidth,
1005
+ numTokens: gridTokens(gridHeight, gridWidth)
1006
+ };
1007
+ }
1008
+ /** Project padded pixel dimensions onto the largest in-budget token grid. */
1009
+ function safeResize(height, width, paddedHeight, paddedWidth) {
1010
+ const gridHeight = gridCells(paddedHeight);
1011
+ const gridWidth = gridCells(paddedWidth);
1012
+ const direct = {
1013
+ gridHeight,
1014
+ gridWidth,
1015
+ bestHeight: paddedHeight,
1016
+ bestWidth: paddedWidth,
1017
+ numTokens: gridTokens(gridHeight, gridWidth)
1018
+ };
1019
+ if (direct.numTokens <= MAX_IMAGE_TOKENS) return direct;
1020
+ const solved = solveResizeRatio(height, width, MAX_IMAGE_TOKENS);
1021
+ /* v8 ignore next 3 -- the published solver's assertion; the closed-form
1022
+ solve stays within the budget for every positive geometry. */
1023
+ if (solved.numTokens > MAX_IMAGE_TOKENS) throw new Error(`deepseek image tokens: no grid fits the token budget for ${width}x${height}`);
1024
+ return solved;
1025
+ }
1026
+ /** One scale-pad-project pass; the caller iterates it to a fixpoint. */
1027
+ function resizeOnce(width, height) {
1028
+ let scaledWidth = width;
1029
+ let scaledHeight = height;
1030
+ const pixels = scaledWidth * scaledHeight;
1031
+ if (pixels < MIN_PIXELS && pixels > 0) {
1032
+ const scale = Math.sqrt(MIN_PIXELS / pixels);
1033
+ scaledWidth = Math.trunc(scaledWidth * scale);
1034
+ scaledHeight = Math.trunc(scaledHeight * scale);
1035
+ }
1036
+ const paddedWidth = ceilDiv(scaledWidth, PATCH_SIZE) * PATCH_SIZE;
1037
+ const paddedHeight = ceilDiv(scaledHeight, PATCH_SIZE) * PATCH_SIZE;
1038
+ return safeResize(scaledHeight, scaledWidth, paddedHeight, paddedWidth);
1039
+ }
1040
+ function sameResize(a, b) {
1041
+ return a.gridHeight === b.gridHeight && a.gridWidth === b.gridWidth && a.bestHeight === b.bestHeight && a.bestWidth === b.bestWidth && a.numTokens === b.numTokens;
1042
+ }
1043
+ /**
1044
+ * Dimensions the harness sends so the provider keeps the whole image: the
1045
+ * source itself when its patch-padded grid fits the token cap, otherwise the
1046
+ * source aspect ratio at the solved grid's long edge. The provider pads the
1047
+ * short edge to whole patches on its side. Rounding the aspect-preserving
1048
+ * short edge can change the token count from the source's solved grid;
1049
+ * request pricing uses the sent dimensions. Small images are never enlarged.
1050
+ * @param width - positive integer source width in pixels.
1051
+ * @param height - positive integer source height in pixels.
1052
+ * @returns the request dimensions to encode.
1053
+ */
1054
+ function deepSeekRequestImageDimensions(width, height) {
1055
+ const paddedWidth = ceilDiv(width, PATCH_SIZE) * PATCH_SIZE;
1056
+ if (gridTokens(gridCells(ceilDiv(height, PATCH_SIZE) * PATCH_SIZE), gridCells(paddedWidth)) <= MAX_IMAGE_TOKENS) return {
1057
+ width,
1058
+ height
1059
+ };
1060
+ const solved = solveResizeRatio(height, width, MAX_IMAGE_TOKENS);
1061
+ return longEdgeDimensions(width, height, width >= height ? solved.bestWidth : solved.bestHeight);
1062
+ }
1063
+ /**
1064
+ * Vision tokens DeepSeek charges for one request image of the given
1065
+ * dimensions.
1066
+ * @param width - positive integer request-image width in pixels.
1067
+ * @param height - positive integer request-image height in pixels.
1068
+ * @returns the provider vision-token price, at most 1024.
1069
+ */
1070
+ function deepSeekImageTokens(width, height) {
1071
+ let result = resizeOnce(width, height);
1072
+ for (let iteration = 1; iteration < 10; iteration += 1) {
1073
+ const next = resizeOnce(result.bestWidth, result.bestHeight);
1074
+ if (sameResize(next, result)) return result.numTokens;
1075
+ result = next;
1076
+ }
1077
+ /* v8 ignore next 2 -- the published solver's non-convergence guard; every
1078
+ pass is a projection, so a second identical pass is a fixpoint. */
1079
+ throw new Error(`deepseek image tokens: resize did not converge for ${width}x${height}`);
1080
+ }
1081
+ //#endregion
1082
+ //#region lib/types/request-pricing.js
1083
+ /**
1084
+ * Provider-side request-image pricing for DeepSeek routes: prices every
1085
+ * retained surface occurrence at its per-model request target with
1086
+ * the published vision-token accounting, and every occurrence the surface
1087
+ * marks offloaded as its placeholder text. Consumed synchronously by the
1088
+ * token meter through `LlmAdapter.imageRequestPricing`; provider usage
1089
+ * remains the authoritative anchor for completed requests.
1090
+ *
1091
+ * @module dsh-llm-deepseek/request-pricing
1092
+ */
1093
+ /** Default bound on accumulated file-referenced image bytes per request. */
1094
+ const DEFAULT_MAX_REQUEST_FILES_BYTES = 128 * 1024 * 1024;
1095
+ /** Provider request image-count limit. */
1096
+ const DEFAULT_MAX_IMAGES_PER_REQUEST = 600;
1097
+ /** Total-pixel budget matching provider low-detail image input. */
1098
+ const DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET = 512 * 512;
1099
+ /** Encoded-byte target for one deterministic model-request image; the smallest quality-ladder output is used when no quality fits. */
1100
+ const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 2 * 1024 * 1024;
1101
+ /**
1102
+ * Provider per-side limit for a request carrying 15 or more images, applied
1103
+ * to every request image so the image count never changes a projection.
1104
+ */
1105
+ const REQUEST_IMAGE_MAX_DIMENSION = 4096;
1106
+ /**
1107
+ * Resolve the encoded-byte target one DeepSeek model route applies to every request image.
1108
+ * @param model - Advertised model route and its optional image overrides.
1109
+ * @returns the route's encoded-byte target.
1110
+ * @internal
1111
+ */
1112
+ function resolveRequestImageMaxBytes(model) {
1113
+ return model.imageMaxBytes ?? 2097152;
1114
+ }
1115
+ /**
1116
+ * Resolve the deterministic request target one DeepSeek model route chooses
1117
+ * for one source image: the published token grid unless the model overrides
1118
+ * it with a pixel budget, then the provider per-side limit, then the route's
1119
+ * encoded-byte target. Small images are never enlarged.
1120
+ * @param model - Advertised model route and its optional image overrides.
1121
+ * @param source - intrinsic dimensions of the normalized attachment.
1122
+ * @returns Complete request dimensions and encoded-byte target.
1123
+ * @internal
1124
+ */
1125
+ function resolveRequestImageTarget(model, source) {
1126
+ const budget = model.imagePixelBudget === "low" ? DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET : model.imagePixelBudget;
1127
+ const projected = budget === void 0 ? deepSeekRequestImageDimensions(source.width, source.height) : requestImageDimensions(source.width, source.height, budget);
1128
+ return {
1129
+ ...Math.max(projected.width, projected.height) > 4096 ? longEdgeDimensions(source.width, source.height, REQUEST_IMAGE_MAX_DIMENSION) : projected,
1130
+ maxBytes: resolveRequestImageMaxBytes(model)
1131
+ };
1132
+ }
1133
+ /**
1134
+ * Price one occurrence a text-only route substitutes with deterministic text,
1135
+ * reproducing the `projectImagesForTextModel` substitution `LlmRuntime`
1136
+ * applies before dispatching to a route without the `image` modality.
1137
+ */
1138
+ function textOnlyPrice(block) {
1139
+ return {
1140
+ visualTokens: 0,
1141
+ text: textOnlyImageText(block.attachment)
1142
+ };
1143
+ }
1144
+ /**
1145
+ * Build the request-image pricing for one DeepSeek route from a validated
1146
+ * connection snapshot. Uncatalogued and text-only models price every
1147
+ * occurrence as its deterministic text substitution; image-capable models
1148
+ * price an offloaded occurrence as its placeholder text and a retained one by
1149
+ * its projected request dimensions, with each occurrence's handle or
1150
+ * placeholder text built through the same access resolution the serializer
1151
+ * uses. Access paths resolve at pricing time, so a path that changes before
1152
+ * the request only shifts the text price by its own length.
1153
+ * @param connection - validated connection facts of the pricing resolution.
1154
+ * @param model - exact model id named by the request header.
1155
+ * @param resolveAccess - current execution-world access resolution shared with request serialization.
1156
+ * @returns synchronous per-occurrence pricing for the route.
1157
+ */
1158
+ function deepSeekImageRequestPricing(connection, model, resolveAccess) {
1159
+ const catalogModel = connection.models.find((entry) => entry.id === model);
1160
+ if (catalogModel?.inputModalities?.includes("image") !== true) return { priceImages: (images) => images.map(textOnlyPrice) };
1161
+ return { priceImages: (images) => images.map(({ attachment: ref, offloaded }) => {
1162
+ if (offloaded === true) return {
1163
+ visualTokens: 0,
1164
+ text: offloadedImageText(ref, resolveAccess?.(ref))
1165
+ };
1166
+ const target = resolveRequestImageTarget(catalogModel, ref);
1167
+ return {
1168
+ visualTokens: deepSeekImageTokens(target.width, target.height),
1169
+ text: requestImageHandleText(ref, target, resolveAccess?.(ref))
1170
+ };
1171
+ }) };
1172
+ }
1173
+ //#endregion
1174
+ //#region lib/types/images.js
1175
+ /** Deterministic Messages image preparation for Files references and bounded inline fallback. */
1176
+ function bounds(connection, representation) {
1177
+ return {
1178
+ representation,
1179
+ maxBytes: representation === "raw" ? connection.maxRequestFilesBytes : connection.maxInlineRequestImageBytes,
1180
+ maxImages: connection.maxImagesPerRequest,
1181
+ byteQuantum: representation === "raw" ? connection.imageOffloadByteQuantum : connection.inlineImageOffloadByteQuantum,
1182
+ countQuantum: connection.imageOffloadCountQuantum
1183
+ };
1184
+ }
1185
+ function* imageRefs(blocks) {
1186
+ for (const block of blocks) if (block.type === "image") yield block.attachment;
1187
+ }
1188
+ /** Normalize retained image references before converting Messages content.
1189
+ * @param history - durable history; never mutated.
1190
+ * @param connection - request-local image budgets.
1191
+ * @param modelId - target model id.
1192
+ * @param attachments - mounted attachment store, required only for image requests.
1193
+ * @param access - current execution-world path resolver.
1194
+ * @param signal - request cancellation.
1195
+ * @returns projected history and prepared image bytes keyed by attachment id.
1196
+ */
1197
+ async function prepareImages(history, connection, modelId, attachments, access, signal) {
1198
+ const versions = /* @__PURE__ */ new Map();
1199
+ const messages = projectOffloadedImages(history, (ref) => offloadedImageText(ref, access(ref)));
1200
+ if (!messages.some((message) => contentHasImage(message.content))) return {
1201
+ messages,
1202
+ versions
1203
+ };
1204
+ const model = connection.models.find((entry) => entry.id === modelId);
1205
+ 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");
1206
+ 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");
1207
+ 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));
1208
+ assertImagesFit(messages, versions, connection, "raw");
1209
+ return {
1210
+ messages,
1211
+ versions
1212
+ };
1213
+ }
1214
+ /** Require logged offload before retrying images that exceed the inline budget.
1215
+ * @param messages - history already within the Files budget.
1216
+ * @param versions - normalized versions prepared for retained references.
1217
+ * @param connection - resolved inline bounds.
1218
+ * @returns unchanged history within both byte and image-count limits.
1219
+ */
1220
+ function inlineImages(messages, versions, connection) {
1221
+ assertImagesFit(messages, versions, connection, "base64");
1222
+ return messages;
1223
+ }
1224
+ /** Count additional oldest occurrences requiring durable offload at their exact represented bytes. */
1225
+ function assertImagesFit(messages, versions, connection, representation) {
1226
+ const offloadImages = requiredImageOffload(messages, bounds(connection, representation), (block) => versions.get(block.attachment.attachmentId).bytes);
1227
+ 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 });
1228
+ }
1229
+ /** Resolve retained images to Files ids, recording every occurrence for failure diagnostics.
1230
+ * @param messages - history within the Files byte/count budget.
1231
+ * @param versions - normalized versions for every retained reference.
1232
+ * @param files - request-owned Files resolution and recovery.
1233
+ * @returns ids keyed by durable attachment identity.
1234
+ */
1235
+ async function prepareFileIds(messages, versions, files) {
1236
+ const ids = /* @__PURE__ */ new Map();
1237
+ for (const [index, message] of messages.entries()) {
1238
+ let image = 0;
1239
+ for (const ref of imageRefs(message.content)) {
1240
+ const version = versions.get(ref.attachmentId);
1241
+ ids.set(ref.attachmentId, await files.resolve(version, {
1242
+ message: index + 1,
1243
+ image: ++image
1244
+ }));
1245
+ }
1246
+ }
1247
+ return ids;
1248
+ }
1249
+ //#endregion
1250
+ //#region lib/types/replay.js
1251
+ /** Minimal native thinking metadata; durable Harness blocks own all response text. */
1252
+ /** Reject malformed JSON objects at provider and durable-data reads.
1253
+ * @param value - untrusted decoded JSON.
1254
+ * @param code - owning failure category.
1255
+ * @returns the validated object.
1256
+ */
1257
+ function object(value, code = "MALFORMED_RESPONSE") {
1258
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new LlmError("DeepSeek Messages expected a JSON object", code);
1259
+ return value;
1260
+ }
1261
+ /** Construct response metadata without duplicating the assistant text.
1262
+ * @param model - requested model identity.
1263
+ * @param blocks - metadata in emitted block order.
1264
+ * @returns the versioned envelope persisted by the existing assembler.
1265
+ */
1266
+ function replayState(model, blocks) {
1267
+ return {
1268
+ response: {
1269
+ kind: "deepseek-messages",
1270
+ version: 1,
1271
+ model
1272
+ },
1273
+ blocks
1274
+ };
1275
+ }
1276
+ /** Validate native replay, discarding unusable metadata before serializing durable content.
1277
+ * @param message - durable assistant content and source metadata.
1278
+ * @param model - target model; cross-model signatures are not portable.
1279
+ * @param onDegrade - diagnostic for unusable metadata; receives no message content or signatures.
1280
+ * @returns index-aligned metadata, absent for foreign, cross-model or degraded history.
1281
+ */
1282
+ function readReplay(message, model, onDegrade) {
1283
+ try {
1284
+ return validateReplay(message, model);
1285
+ } catch (error) {
1286
+ /* v8 ignore next -- the validator only throws INVALID_REPLAY_STATE; preserve future non-replay failures. */
1287
+ if (!(error instanceof LlmError) || error.code !== "INVALID_REPLAY_STATE") throw error;
1288
+ onDegrade?.(error.message);
1289
+ return;
1290
+ }
1291
+ }
1292
+ function validateReplay(message, model) {
1293
+ if (message.source.kind !== "model" || message.source.replayState === void 0) return void 0;
1294
+ const fail = (detail) => {
1295
+ throw new LlmError(`DeepSeek Messages replay: ${detail}`, "INVALID_REPLAY_STATE");
1296
+ };
1297
+ const envelope = object(message.source.replayState, "INVALID_REPLAY_STATE");
1298
+ const response = object(envelope.response, "INVALID_REPLAY_STATE");
1299
+ if (response.kind !== "deepseek-messages" || response.version !== 1) return fail("unsupported kind or version");
1300
+ if (response.model !== message.source.model) return fail("model does not match assistant source model");
1301
+ if (!Array.isArray(envelope.blocks) || envelope.blocks.length !== message.content.length) return fail("block count mismatch");
1302
+ const blocks = envelope.blocks.map((value, index) => {
1303
+ const block = object(value, "INVALID_REPLAY_STATE");
1304
+ if (block.type !== message.content[index]?.type || block.type !== "text" && block.type !== "reasoning" && block.type !== "tool-call") return fail("block type mismatch");
1305
+ if (block.signature !== void 0 && (block.type !== "reasoning" || typeof block.signature !== "string")) return fail("invalid signature");
1306
+ return {
1307
+ type: block.type,
1308
+ ...typeof block.signature === "string" ? { signature: block.signature } : {}
1309
+ };
1310
+ });
1311
+ return response.model === model ? blocks : void 0;
1312
+ }
1313
+ //#endregion
1314
+ //#region lib/types/serialize.js
1315
+ /** Map system snapshots and conversation turns to Messages using the configured route capability. */
1316
+ function unsupported(type) {
1317
+ throw new LlmError(`DeepSeek Messages cannot represent ${type}`, "UNSUPPORTED_CONTENT");
1318
+ }
1319
+ /** Historical arguments that Messages cannot represent use empty input; durable content stays unchanged. */
1320
+ function toolInput(raw) {
1321
+ let value;
1322
+ try {
1323
+ value = JSON.parse(raw);
1324
+ } catch (_invalidToolHistoryJson) {
1325
+ return {};
1326
+ }
1327
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1328
+ }
1329
+ function assistant(message, model, onReplayDegrade) {
1330
+ const replay = readReplay(message, model, onReplayDegrade);
1331
+ return message.content.map((block, index) => {
1332
+ switch (block.type) {
1333
+ case "text": return {
1334
+ type: "text",
1335
+ text: block.text
1336
+ };
1337
+ case "reasoning": return {
1338
+ type: "thinking",
1339
+ thinking: block.text,
1340
+ ...replay?.[index]?.signature === void 0 ? {} : { signature: replay[index].signature }
1341
+ };
1342
+ case "tool-call": return {
1343
+ type: "tool_use",
1344
+ id: block.id,
1345
+ name: block.name,
1346
+ input: toolInput(block.arguments)
1347
+ };
1348
+ default: return unsupported(`assistant content ${block.type}`);
1349
+ }
1350
+ });
1351
+ }
1352
+ /** Serialize one complete request using already prepared image bytes.
1353
+ * User and tool-result content omits reasoning and tool-call blocks.
1354
+ * Empty user messages are skipped; empty tool results retain their call ids.
1355
+ * @param options - provider-neutral request.
1356
+ * @param connection - validated defaults and thinking policy.
1357
+ * @param history - image-projected history with complete system snapshots; durable messages remain unchanged.
1358
+ * @param images - request versions for retained images.
1359
+ * @param access - execution-world paths for image descriptions.
1360
+ * @param onReplayDegrade - diagnostic for discarded native replay metadata.
1361
+ * @param fileIds - resolved Files references; omission selects inline image bytes.
1362
+ * @returns the Messages API JSON body.
1363
+ */
1364
+ function serialize(options, connection, history, images, access, onReplayDegrade, fileIds) {
1365
+ const model = connection.models.find((entry) => entry.id === options.model);
1366
+ const inHistory = model?.systemPromptUpdate === "in-history";
1367
+ const input = (blocks) => blocks.flatMap((block) => {
1368
+ if (block.type === "text") return block.text ? [{
1369
+ type: "text",
1370
+ text: block.text
1371
+ }] : [];
1372
+ if (block.type === "reasoning" || block.type === "tool-call") return [];
1373
+ if (block.type !== "image") return unsupported(`user/tool-result content ${block.type}`);
1374
+ const version = images.get(block.attachment.attachmentId);
1375
+ if (version === void 0) throw new LlmError("DeepSeek Messages request image is missing", "INVALID_REQUEST");
1376
+ const fileId = fileIds?.get(block.attachment.attachmentId);
1377
+ if (fileIds !== void 0 && fileId === void 0) throw new LlmError("DeepSeek Messages request file id is missing", "INVALID_REQUEST");
1378
+ return [{
1379
+ type: "text",
1380
+ text: requestImageHandleText(block.attachment, version, access(block.attachment))
1381
+ }, fileId === void 0 ? {
1382
+ type: "image",
1383
+ source: {
1384
+ type: "base64",
1385
+ media_type: version.mediaType,
1386
+ data: Buffer.from(version.data).toString("base64")
1387
+ }
1388
+ } : {
1389
+ type: "image",
1390
+ source: {
1391
+ type: "file",
1392
+ file_id: fileId
1393
+ }
1394
+ }];
1395
+ });
1396
+ const messages = [];
1397
+ let historySystem;
1398
+ const systemUpdates = [];
1399
+ const flushSystemUpdates = () => {
1400
+ if (systemUpdates.length === 0) return;
1401
+ if (messages.at(-1)?.role !== "user") return unsupported("system update without a preceding user or tool-result turn");
1402
+ messages.push(...systemUpdates.splice(0));
1403
+ };
1404
+ if (options.tools?.some((tool) => tool.deferLoading === true)) return unsupported("deferred tool loading");
1405
+ for (const message of history) {
1406
+ if (message.role === "developer") return unsupported("developer message");
1407
+ if (message.content.some((block) => block.type === "tool-addition" || block.type === "tool-removal")) return unsupported("tool-change blocks outside developer messages");
1408
+ if (message.role === "system") {
1409
+ const texts = message.content.filter((block) => block.type === "text");
1410
+ if (texts.length !== message.content.length) return unsupported("non-text system message");
1411
+ const text = texts.map((block) => block.text).join("");
1412
+ if (inHistory && messages.length > 0) {
1413
+ if (text.length === 0) return unsupported("empty in-history system update");
1414
+ systemUpdates.push({
1415
+ role: "system",
1416
+ content: [{
1417
+ type: "text",
1418
+ text
1419
+ }]
1420
+ });
1421
+ } else historySystem = text;
1422
+ continue;
1423
+ }
1424
+ if (message.role === "assistant") flushSystemUpdates();
1425
+ const content = message.role === "assistant" ? assistant(message, options.model, onReplayDegrade) : message.role === "tool" ? [{
1426
+ type: "tool_result",
1427
+ tool_use_id: message.toolCallId,
1428
+ content: input(message.content),
1429
+ ...message.isError === void 0 ? {} : { is_error: message.isError }
1430
+ }] : message.content.flatMap((block) => input([block]));
1431
+ if (message.role === "user" && content.length === 0) continue;
1432
+ const wireRole = message.role === "tool" ? "user" : message.role;
1433
+ const previous = messages.at(-1);
1434
+ if (previous?.role === wireRole) previous.content.push(...content);
1435
+ else messages.push({
1436
+ role: wireRole,
1437
+ content
1438
+ });
1439
+ }
1440
+ flushSystemUpdates();
1441
+ let pending = /* @__PURE__ */ new Set();
1442
+ for (const message of messages) if (message.role === "assistant") {
1443
+ const calls = message.content.filter((block) => block.type === "tool_use");
1444
+ pending = new Set(calls.map((block) => block.id));
1445
+ if (pending.size !== calls.length) throw new LlmError("DeepSeek Messages duplicate tool call id", "INVALID_REQUEST");
1446
+ } else if (message.role === "user") {
1447
+ const results = message.content.filter((block) => block.type === "tool_result");
1448
+ 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");
1449
+ if (pending.size > 0) throw new LlmError("DeepSeek Messages tool calls need immediate results", "INVALID_REQUEST");
1450
+ message.content = [...results, ...message.content.filter((block) => block.type !== "tool_result")];
1451
+ }
1452
+ if (pending.size > 0) throw new LlmError("DeepSeek Messages history ends with unresolved tools", "INVALID_REQUEST");
1453
+ const requestedEffort = options.purpose === "session-title" ? "off" : options.reasoningEffort ?? connection.defaults.reasoningEffort ?? (connection.defaults.thinking === "disabled" ? "off" : "high");
1454
+ const effort = requestedEffort === "ultra" ? "max" : requestedEffort;
1455
+ if (![
1456
+ "off",
1457
+ "low",
1458
+ "high",
1459
+ "max"
1460
+ ].includes(effort) || connection.defaults.thinking === "disabled" && effort !== "off") throw new LlmError(`DeepSeek Messages does not support reasoning effort ${effort}`, "UNSUPPORTED_REASONING_EFFORT");
1461
+ const system = ultraSystem(options, flashSystem(options, [options.system, historySystem].filter(Boolean).join("\n\n")));
1462
+ return {
1463
+ model: options.model,
1464
+ stream: true,
1465
+ messages,
1466
+ max_tokens: options.maxTokens ?? model?.maxTokens ?? connection.maxTokens,
1467
+ thinking: { type: effort === "off" ? "disabled" : "enabled" },
1468
+ ...effort === "off" ? {} : { output_config: { effort } },
1469
+ ...system.length === 0 ? {} : { system },
1470
+ ...options.temperature === void 0 ? {} : { temperature: options.temperature },
1471
+ ...options.stop === void 0 ? {} : { stop_sequences: options.stop },
1472
+ ...options.tools === void 0 ? {} : { tools: options.tools.filter((tool) => tool.name !== "workflow" && tool.name !== "ralph").map((tool) => ({
1473
+ name: tool.name,
1474
+ description: tool.description,
1475
+ input_schema: tool.parameters
1476
+ })) }
1477
+ };
1478
+ }
1479
+ //#endregion
1480
+ //#region lib/types/transport.js
1481
+ /** Normalize HTTP and in-band Messages errors into provider-neutral failures. */
1482
+ /** Read only provider error fields used by bounded Files recovery.
1483
+ * @param raw - decoded HTTP error response.
1484
+ * @returns code, type, and message text, without unrelated response fields.
1485
+ */
1486
+ function providerErrorDetail(raw) {
1487
+ const error = typeof raw === "object" && raw !== null && "error" in raw ? raw.error : void 0;
1488
+ if (typeof error !== "object" || error === null) return "";
1489
+ const fields = error;
1490
+ return [
1491
+ fields.code,
1492
+ fields.type,
1493
+ fields.message
1494
+ ].filter((value) => typeof value === "string").join(" ");
1495
+ }
1496
+ /** Classify a provider error without trusting arbitrary response fields.
1497
+ * @param raw - decoded response or in-band error event.
1498
+ * @param status - HTTP status when the error preceded streaming.
1499
+ * @param headers - response headers for retry delay and request identity.
1500
+ * @returns a stable error consumed by LlmRuntime and llm-retry.
1501
+ */
1502
+ function providerError(raw, status, headers) {
1503
+ const envelope = typeof raw === "object" && raw !== null ? raw : {};
1504
+ const error = typeof envelope.error === "object" && envelope.error !== null ? envelope.error : {};
1505
+ const message = typeof error.message === "string" ? error.message : `DeepSeek Messages request failed (${status ?? "stream error"})`;
1506
+ const type = typeof error.type === "string" ? error.type : "";
1507
+ const detail = `${type} ${typeof error.code === "string" ? error.code : ""} ${message}`;
1508
+ let code;
1509
+ if (status === 401 || status === 403 || ["authentication_error", "permission_error"].includes(type)) code = "AUTH";
1510
+ else if (isQuotaExceededError(detail) || status === 402) code = "QUOTA";
1511
+ else if (status === 429 || type === "rate_limit_error") code = "RATE_LIMIT";
1512
+ else if (isContextWindowExceededError(detail)) code = "CONTEXT_WINDOW_EXCEEDED";
1513
+ else if (status === 400 || status === 413 || type === "invalid_request_error") code = "INVALID_REQUEST";
1514
+ else if (status !== void 0 && status >= 500 || ["api_error", "overloaded_error"].includes(type)) code = "SERVER";
1515
+ else code = status === void 0 ? "SERVER" : `HTTP_${status}`;
1516
+ const retry = headers?.get("retry-after");
1517
+ const delay = retry == null ? NaN : /^\d+(?:\.\d+)?$/u.test(retry) ? Number(retry) * 1e3 : Date.parse(retry) - Date.now();
1518
+ const id = headers?.get("request-id") ?? headers?.get("x-request-id") ?? headers?.get("x-deepseek-request-id");
1519
+ return new LlmError(message, code, {
1520
+ ...status === void 0 ? {} : { status },
1521
+ ...id ? { requestId: ProviderRequestId(id) } : {},
1522
+ ...Number.isFinite(delay) && delay > 0 ? { providerRetryAfterMs: delay } : {}
1523
+ });
1524
+ }
1525
+ //#endregion
1526
+ //#region lib/types/sse.js
1527
+ /** SSE framing delegated to eventsource-parser; JSON errors remain provider failures. */
1528
+ /** Decode complete SSE frames without treating an unterminated tail as an event.
1529
+ * @param body - provider response bytes.
1530
+ * @param activity - pulse the idle watchdog for events and heartbeat comments.
1531
+ * @returns JSON events, including message_stop; the translator owns completion.
1532
+ */
1533
+ async function* parseSse(body, activity) {
1534
+ const events = body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream({ onComment: activity }));
1535
+ for await (const frame of events) {
1536
+ activity();
1537
+ let raw;
1538
+ try {
1539
+ raw = JSON.parse(frame.data);
1540
+ } catch (_invalidSseJson) {
1541
+ throw new LlmError("DeepSeek Messages SSE contains invalid JSON", "MALFORMED_RESPONSE");
1542
+ }
1543
+ const event = object(raw);
1544
+ if (typeof event.type !== "string" || frame.event !== void 0 && frame.event !== event.type) throw new LlmError("DeepSeek Messages SSE event type mismatch", "MALFORMED_RESPONSE");
1545
+ if (event.type === "error") throw providerError(event, void 0);
1546
+ yield event;
1547
+ }
1548
+ }
1549
+ //#endregion
1550
+ //#region lib/types/translate.js
1551
+ /** Translate Messages events while preserving block order and cumulative usage. */
1552
+ /** Decode a required string from provider JSON.
1553
+ * @param value - provider field.
1554
+ * @returns the validated string.
1555
+ */
1556
+ function string(value) {
1557
+ if (typeof value !== "string") throw new LlmError("DeepSeek Messages expected a string field", "MALFORMED_RESPONSE");
1558
+ return value;
1559
+ }
1560
+ function malformed(detail) {
1561
+ throw new LlmError(`DeepSeek Messages stream: ${detail}`, "MALFORMED_RESPONSE");
1562
+ }
1563
+ function indexOf(event) {
1564
+ if (!Number.isSafeInteger(event.index) || event.index < 0) return malformed("invalid block index");
1565
+ return event.index;
1566
+ }
1567
+ function updateUsage(usage, raw) {
1568
+ const fields = object(raw);
1569
+ for (const [wire, local] of Object.entries({
1570
+ input_tokens: "inputTokens",
1571
+ output_tokens: "outputTokens",
1572
+ cache_read_input_tokens: "cacheReadTokens",
1573
+ cache_creation_input_tokens: "cacheWriteTokens"
1574
+ })) {
1575
+ const value = fields[wire];
1576
+ if (value === void 0) continue;
1577
+ if (!Number.isSafeInteger(value) || value < 0) return malformed(`invalid ${wire}`);
1578
+ usage[local] = value;
1579
+ }
1580
+ }
1581
+ function startBlock(event, index) {
1582
+ const native = object(event.content_block);
1583
+ let content;
1584
+ let replay;
1585
+ switch (native.type) {
1586
+ case "text":
1587
+ content = {
1588
+ type: "text",
1589
+ text: string(native.text)
1590
+ };
1591
+ replay = { type: "text" };
1592
+ break;
1593
+ case "thinking":
1594
+ content = {
1595
+ type: "reasoning",
1596
+ text: string(native.thinking)
1597
+ };
1598
+ replay = {
1599
+ type: "reasoning",
1600
+ ...native.signature === void 0 ? {} : { signature: string(native.signature) }
1601
+ };
1602
+ break;
1603
+ case "tool_use":
1604
+ content = {
1605
+ type: "tool-call",
1606
+ id: ToolCallId(string(native.id)),
1607
+ name: string(native.name),
1608
+ arguments: JSON.stringify(object(native.input))
1609
+ };
1610
+ if (!content.id || !content.name) return malformed("empty tool identity");
1611
+ replay = { type: "tool-call" };
1612
+ break;
1613
+ default: throw new LlmError(`DeepSeek Messages does not support response block ${String(native.type)}`, "UNSUPPORTED_CONTENT");
1614
+ }
1615
+ return {
1616
+ index,
1617
+ content,
1618
+ replay,
1619
+ closed: false,
1620
+ json: ""
1187
1621
  };
1188
1622
  }
1189
- /**
1190
- * Accept one streamed identity field for a tool call. `id` and `name` are
1191
- * identity, not accumulation: the wire sends each once, on the call's first
1192
- * delta. A continuation delta that re-sends the field empty — or `null`, which
1193
- * some OpenAI-compatible gateways fill in — means "no update", never "clear".
1194
- * @param current - the identity established by an earlier delta of this call.
1195
- * @param incoming - the field as parsed from this delta. The wire type is a
1196
- * claim about a remote encoder, so anything but a non-empty string leaves the
1197
- * established value alone rather than overwriting it.
1198
- * @returns the identity in force after this delta.
1199
- */
1200
- function acceptIdentity(current, incoming) {
1201
- return typeof incoming === "string" && incoming.length > 0 ? incoming : current;
1202
- }
1203
- /** Assemble the final ContentBlock for one open block. */
1204
- function closeBlock(block) {
1205
- switch (block.kind) {
1206
- case "text": return {
1207
- type: "text",
1208
- text: block.text
1623
+ function deltaChunk(block, raw) {
1624
+ const delta = object(raw);
1625
+ const content = block.content;
1626
+ if (delta.type === "text_delta" && content.type === "text") {
1627
+ const text = string(delta.text);
1628
+ content.text += text;
1629
+ return {
1630
+ type: "text-delta",
1631
+ index: block.index,
1632
+ text
1209
1633
  };
1210
- case "reasoning": return {
1211
- type: "reasoning",
1212
- text: block.text
1634
+ }
1635
+ if (delta.type === "thinking_delta" && content.type === "reasoning") {
1636
+ const text = string(delta.thinking);
1637
+ content.text += text;
1638
+ return {
1639
+ type: "reasoning-delta",
1640
+ index: block.index,
1641
+ text
1213
1642
  };
1214
- case "tool-call": return {
1215
- type: "tool-call",
1216
- id: brandString(block.callId ?? ""),
1217
- name: block.name ?? "",
1218
- arguments: block.text
1643
+ }
1644
+ if (delta.type === "signature_delta" && content.type === "reasoning") {
1645
+ block.replay.signature = (block.replay.signature ?? "") + string(delta.signature);
1646
+ return;
1647
+ }
1648
+ if (delta.type === "input_json_delta" && content.type === "tool-call") {
1649
+ const argumentsDelta = string(delta.partial_json);
1650
+ block.json += argumentsDelta;
1651
+ return {
1652
+ type: "tool-call-delta",
1653
+ index: block.index,
1654
+ id: content.id,
1655
+ argumentsDelta
1219
1656
  };
1220
1657
  }
1658
+ return malformed(`unsupported delta ${String(delta.type)} for ${content.type}`);
1221
1659
  }
1222
- /**
1223
- * Consume SSE data payloads (ending with `[DONE]`) and yield StreamChunks.
1224
- * Malformed JSON payloads abort the stream with `MALFORMED_RESPONSE`.
1225
- * @param payloads - SSE data payloads from {@link parseSse}, `[DONE]`-terminated.
1226
- * @returns deltas as they arrive; `block-end`s, `usage`, and `finish` are all deferred to the `[DONE]` sentinel.
1227
- * A `stop` (or absent) finish with no opened blocks is a degenerate provider completion and maps to an
1228
- * `EMPTY_RESPONSE` error finish instead of a successful empty message.
1229
- */
1230
- async function* translate(payloads) {
1231
- let nextIndex = 0;
1232
- let textBlock;
1233
- let reasoningBlock;
1234
- const toolBlocks = /* @__PURE__ */ new Map();
1235
- const order = [];
1236
- let pendingFinish;
1237
- let pendingUsage;
1238
- function open(kind) {
1239
- const block = {
1240
- index: nextIndex++,
1241
- kind,
1242
- text: ""
1243
- };
1244
- order.push(block);
1245
- return block;
1660
+ function stopReason(raw) {
1661
+ switch (raw) {
1662
+ case "end_turn":
1663
+ case "stop_sequence": return { kind: "stop" };
1664
+ case "tool_use": return { kind: "tool-calls" };
1665
+ case "max_tokens": return { kind: "max-tokens" };
1666
+ default: return malformed(`unsupported stop reason ${String(raw)}`);
1246
1667
  }
1247
- for await (const payload of payloads) {
1248
- if (payload === "[DONE]") {
1249
- for (const block of order) yield {
1250
- type: "block-end",
1251
- index: block.index,
1252
- block: closeBlock(block)
1253
- };
1254
- if (pendingUsage) yield {
1255
- type: "usage",
1256
- usage: pendingUsage
1257
- };
1258
- const reason = pendingFinish ?? { kind: "stop" };
1668
+ }
1669
+ /** Translate decoded SSE data into the Harness stream protocol.
1670
+ * @param events - framed, decoded provider events in arrival order.
1671
+ * @param model - requested model id stored in durable replay state.
1672
+ * @returns blocks, one final usage value, and exactly one terminal finish.
1673
+ */
1674
+ async function* translate(events, model) {
1675
+ const blocks = /* @__PURE__ */ new Map();
1676
+ const usage = {
1677
+ inputTokens: 0,
1678
+ outputTokens: 0
1679
+ };
1680
+ let started = false;
1681
+ let reason;
1682
+ for await (const event of events) {
1683
+ if (event.type === "message_start") {
1684
+ if (started) return malformed("duplicate message_start");
1685
+ updateUsage(usage, object(event.message).usage);
1686
+ started = true;
1687
+ continue;
1688
+ }
1689
+ if (![
1690
+ "content_block_start",
1691
+ "content_block_delta",
1692
+ "content_block_stop",
1693
+ "message_delta",
1694
+ "message_stop"
1695
+ ].includes(String(event.type))) continue;
1696
+ if (!started) return malformed("event precedes message_start");
1697
+ if (event.type === "content_block_start") {
1698
+ const wireIndex = indexOf(event);
1699
+ if (blocks.has(wireIndex) || reason !== void 0) return malformed("block starts after settlement or repeats an index");
1700
+ const block = startBlock(event, blocks.size);
1701
+ blocks.set(wireIndex, block);
1259
1702
  yield {
1260
- type: "finish",
1261
- reason: reason.kind === "stop" && order.length === 0 ? {
1262
- kind: "error",
1263
- failure: {
1264
- message: "model returned a completed response with no content",
1265
- code: EMPTY_RESPONSE_CODE
1266
- }
1267
- } : reason
1703
+ type: "block-start",
1704
+ index: block.index,
1705
+ blockType: block.content.type
1268
1706
  };
1269
- return;
1270
- }
1271
- let chunk;
1272
- try {
1273
- chunk = JSON.parse(payload);
1274
- } catch {
1275
- throw new LlmError(`malformed SSE payload: ${payload.slice(0, 120)}`, "MALFORMED_RESPONSE");
1276
- }
1277
- for (const choice of chunk.choices ?? []) {
1278
- const delta = choice.delta;
1279
- const reasoning = delta?.reasoning_content;
1280
- if (typeof reasoning === "string" && reasoning.length > 0) {
1281
- if (!reasoningBlock) {
1282
- reasoningBlock = open("reasoning");
1283
- yield {
1284
- type: "block-start",
1285
- index: reasoningBlock.index,
1286
- blockType: "reasoning"
1287
- };
1288
- }
1289
- reasoningBlock.text += reasoning;
1290
- yield {
1291
- type: "reasoning-delta",
1292
- index: reasoningBlock.index,
1293
- text: reasoning
1707
+ if (block.content.type === "text" || block.content.type === "reasoning") {
1708
+ if (block.content.text) yield {
1709
+ type: block.content.type === "text" ? "text-delta" : "reasoning-delta",
1710
+ index: block.index,
1711
+ text: block.content.text
1294
1712
  };
1295
- }
1296
- const content = delta?.content;
1297
- if (typeof content === "string" && content.length > 0) {
1298
- if (!textBlock) {
1299
- textBlock = open("text");
1300
- yield {
1301
- type: "block-start",
1302
- index: textBlock.index,
1303
- blockType: "text"
1304
- };
1305
- }
1306
- textBlock.text += content;
1713
+ } else yield {
1714
+ type: "tool-call-delta",
1715
+ index: block.index,
1716
+ id: block.content.id,
1717
+ name: block.content.name,
1718
+ argumentsDelta: ""
1719
+ };
1720
+ } else if (event.type === "content_block_delta" || event.type === "content_block_stop") {
1721
+ const block = blocks.get(indexOf(event));
1722
+ if (block === void 0 || block.closed) return malformed("delta/stop without an open block");
1723
+ if (event.type === "content_block_delta") {
1724
+ const chunk = deltaChunk(block, event.delta);
1725
+ if (chunk !== void 0) yield chunk;
1726
+ } else {
1727
+ block.closed = true;
1728
+ if (block.content.type === "tool-call" && block.json.length > 0) block.content.arguments = block.json;
1307
1729
  yield {
1308
- type: "text-delta",
1309
- index: textBlock.index,
1310
- text: content
1730
+ type: "block-end",
1731
+ index: block.index,
1732
+ block: { ...block.content }
1311
1733
  };
1312
1734
  }
1313
- for (const call of delta?.tool_calls ?? []) {
1314
- let block = toolBlocks.get(call.index);
1315
- if (!block) {
1316
- block = open("tool-call");
1317
- toolBlocks.set(call.index, block);
1318
- yield {
1319
- type: "block-start",
1320
- index: block.index,
1321
- blockType: "tool-call"
1322
- };
1735
+ } else if (event.type === "message_delta") {
1736
+ const delta = object(event.delta);
1737
+ if (delta.stop_reason != null) reason = stopReason(delta.stop_reason);
1738
+ if (event.usage !== void 0) updateUsage(usage, event.usage);
1739
+ } else {
1740
+ if (reason === void 0 || [...blocks.values()].some((block) => !block.closed)) return malformed("message_stop without settled blocks and stop reason");
1741
+ if (blocks.size === 0 && reason.kind === "stop") throw new LlmError("DeepSeek Messages returned no content", "EMPTY_RESPONSE");
1742
+ if (reason.kind !== "max-tokens") for (const { content } of blocks.values()) {
1743
+ if (content.type !== "tool-call") continue;
1744
+ let parsed;
1745
+ try {
1746
+ parsed = JSON.parse(content.arguments);
1747
+ } catch (_invalidProviderToolJson) {
1748
+ return malformed("tool input is invalid JSON");
1323
1749
  }
1324
- block.callId = acceptIdentity(block.callId, call.id);
1325
- block.name = acceptIdentity(block.name, call.function?.name);
1326
- const fragment = call.function?.arguments ?? "";
1327
- block.text += fragment;
1328
- yield {
1329
- type: "tool-call-delta",
1330
- index: block.index,
1331
- id: brandString(block.callId ?? ""),
1332
- ...block.name !== void 0 ? { name: block.name } : {},
1333
- argumentsDelta: fragment
1334
- };
1750
+ object(parsed);
1335
1751
  }
1336
- if (typeof choice.finish_reason === "string") pendingFinish = mapFinishReason(choice.finish_reason);
1752
+ usage.totalTokens = usage.inputTokens + usage.outputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
1753
+ yield {
1754
+ type: "usage",
1755
+ usage
1756
+ };
1757
+ yield {
1758
+ type: "finish",
1759
+ reason,
1760
+ replayState: replayState(model, [...blocks.values()].map((block) => block.replay))
1761
+ };
1762
+ return;
1337
1763
  }
1338
- if (chunk.usage) pendingUsage = mapUsage(chunk.usage);
1339
1764
  }
1340
- throw new LlmError("SSE payload stream ended without [DONE]", "STREAM_CLOSED");
1765
+ throw new LlmError("DeepSeek Messages stream ended before message_stop", "STREAM_CLOSED");
1341
1766
  }
1342
1767
  //#endregion
1343
1768
  //#region lib/types/adapter.js
1344
- /**
1345
- * `DeepSeekAdapter`: fetch + SSE against a DeepSeek (OpenAI-compatible)
1346
- * chat-completions endpoint, emitting harness StreamChunks. The adapter is
1347
- * transport-only: connection facts arrive through a thunk resolved once per
1348
- * operation and the bearer token through a per-request resolver, so the
1349
- * registering plugin owns validation, layering, and credential policy.
1350
- *
1351
- * @module dsh-llm-deepseek/adapter
1352
- */
1769
+ /** Direct Messages transport with one cancellable lifecycle per model request. */
1353
1770
  var __addDisposableResource = function(env, value, async) {
1354
1771
  if (value !== null && value !== void 0) {
1355
1772
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
@@ -1408,175 +1825,18 @@ var __disposeResources = (function(SuppressedError) {
1408
1825
  var e = new Error(message);
1409
1826
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1410
1827
  });
1411
- /** Default maximum idle interval while an adapter stream read is outstanding. */
1412
- const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
1413
- /** Default combined request/response context capacity. */
1414
- const DEFAULT_CONTEXT_WINDOW = 1e6;
1415
- /** Default per-request output-token cap. */
1416
- const DEFAULT_MAX_TOKENS = 256e3;
1417
- /** Default bound on accumulated base64 image payload after Files API fallback. */
1418
- const DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
1419
- /** Deterministic raw-byte removal step. */
1420
- const DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM = 64 * 1024 * 1024;
1421
- /** Deterministic base64-byte removal step after Files API fallback. */
1422
- const DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM = 10 * 1024 * 1024;
1423
- /** Deterministic image-count removal step. */
1424
- const DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM = 20;
1425
- /** Default explicit lifetime for uploaded images. */
1426
- const DEFAULT_FILE_EXPIRY_SECONDS = 10080 * 60;
1427
- /** Default proactive refresh window for indexed file ids. */
1428
- const DEFAULT_FILE_REFRESH_MARGIN_SECONDS = 3600;
1429
- /** Default number of oldest harness-owned files removed on quota recovery. */
1430
- const DEFAULT_FILE_QUOTA_CLEANUP_BATCH = 100;
1431
- /** Default deadline for resolving one request image through the Files API. */
1432
- const DEFAULT_FILES_API_TIMEOUT_MS = 6e4;
1433
- const STREAM_IDLE_TIMEOUT_CODE = "LLM_STREAM_IDLE_TIMEOUT";
1434
- const FILES_API_TIMEOUT_CODE = "DEEPSEEK_FILES_API_TIMEOUT";
1435
- const OFF_REASONING_EFFORT = ReasoningEffortId("off");
1436
- const LOW_REASONING_EFFORT = ReasoningEffortId("low");
1437
- const HIGH_REASONING_EFFORT = ReasoningEffortId("high");
1438
- const MAX_REASONING_EFFORT = ReasoningEffortId("max");
1439
- const REASONING_EFFORTS = [
1440
- { id: ReasoningEffortId("ultra"), name: "Ultra", description: "DSCODE: max reasoning plus deliberate subagent collaboration; higher total token use." },
1441
- {
1442
- id: OFF_REASONING_EFFORT,
1443
- name: "Off",
1444
- description: "Use for simple tasks that do not need reasoning."
1445
- },
1446
- {
1447
- id: LOW_REASONING_EFFORT,
1448
- name: "Low",
1449
- description: "Prefer for routine or latency-sensitive tasks."
1450
- },
1451
- {
1452
- id: HIGH_REASONING_EFFORT,
1453
- name: "High",
1454
- description: "The default balance for most tasks."
1455
- },
1456
- {
1457
- id: MAX_REASONING_EFFORT,
1458
- name: "Max",
1459
- description: "Reserve for the hardest quality-first tasks."
1460
- }
1461
- ];
1462
- const OFF_ONLY_REASONING_EFFORTS = [{
1463
- id: OFF_REASONING_EFFORT,
1464
- name: "Off",
1465
- description: "Use for simple tasks that do not need reasoning."
1466
- }];
1467
- /** Marks a failed file-id resolution that may be retried as an inline request. */
1468
- var FileResolutionFailure = class extends Error {
1469
- constructor(cause) {
1470
- super("DeepSeek Files API could not resolve a request image.", { cause });
1471
- this.name = "FileResolutionFailure";
1472
- }
1473
- };
1474
- function collectImageRefs(content, refs) {
1475
- for (const block of content) if (block.type === "image") refs.set(block.attachment.attachmentId, block.attachment);
1476
- else if (block.type === "tool-result") collectImageRefs(block.content, refs);
1477
- }
1478
- async function prepareRequestImages(options, attachments, model, signal) {
1479
- const refs = /* @__PURE__ */ new Map();
1480
- for (const message of options.messages) collectImageRefs(message.content, refs);
1481
- const policy = resolveRequestImagePolicy(model);
1482
- const orderedRefs = [...refs.values()];
1483
- const projected = await Promise.all(orderedRefs.map((ref) => attachments.readImageRequest(ref, policy, signal)));
1484
- return new Map(orderedRefs.map((ref, index) => [ref.attachmentId, projected[index]]));
1485
- }
1486
- function providerRejectedNormalizedImage(detail) {
1487
- 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);
1488
- }
1489
- function providerRejectedFileId(detail) {
1490
- const file = /\bfile(?:[_ -]?(?:id|api|not[_ -]?found|deleted|expired))?/iu.test(detail);
1491
- const missing = /(?:expired|not[_ -]?found|deleted|do(?:es)? not exist|not created under (?:this|your) account)/iu.test(detail);
1492
- const invalidId = /(?:invalid.{0,20}file[_ -]?(?:id|api)|file[_ -]?(?:id|api).{0,20}invalid)/iu.test(detail);
1493
- return file && (missing || invalidId);
1494
- }
1495
- function detailNamesFileId(detail, fileId) {
1496
- let index = detail.indexOf(fileId);
1497
- while (index >= 0) {
1498
- const before = detail[index - 1];
1499
- const after = detail[index + fileId.length];
1500
- if ((before === void 0 || !/[\p{L}\p{N}_-]/u.test(before)) && (after === void 0 || !/[\p{L}\p{N}_-]/u.test(after))) return true;
1501
- index = detail.indexOf(fileId, index + 1);
1502
- }
1503
- return false;
1504
- }
1505
- function staleMappings(files, detail) {
1506
- const unique = [...new Map(files.map((file) => [`${file.version.variantId}\0${file.fileId}`, file])).values()];
1507
- const exact = unique.filter((file) => detailNamesFileId(detail, file.fileId));
1508
- return exact.length > 0 ? exact : unique;
1509
- }
1510
- function normalizedImageFacts(file) {
1511
- const version = file.version;
1512
- const name = version.attachment.name ?? version.attachment.attachmentId;
1513
- const colour = version.hasAlpha ? "sRGBA" : "sRGB";
1514
- return `"${name}" at message ${file.location.message}, image ${file.location.image} (${version.mediaType}, 8-bit ${colour}, ${version.width}x${version.height})`;
1515
- }
1516
- function normalizedImageDiagnostic(files, providerMessage, providerDetail) {
1517
- const target = files.find((file) => detailNamesFileId(providerDetail, file.fileId)) ?? (files.length === 1 ? files[0] : void 0);
1518
- 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.`;
1519
- 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.`;
1520
- }
1521
- function modelInfo(provider, model) {
1522
- return {
1523
- provider,
1524
- id: model.id,
1525
- name: model.name ?? model.id,
1526
- ...model.description === void 0 ? {} : { description: model.description },
1527
- inputModalities: model.inputModalities ?? ["text"]
1528
- };
1529
- }
1530
- function providerRetryAfterMs(value) {
1531
- if (value === null) return void 0;
1532
- if (/^\d+$/.test(value)) {
1533
- const delay = Number(value) * 1e3;
1534
- return Number.isFinite(delay) && delay > 0 ? delay : void 0;
1535
- }
1536
- const delay = Date.parse(value) - Date.now();
1537
- return Number.isFinite(delay) && delay > 0 ? delay : void 0;
1538
- }
1539
- function requestId(headers) {
1540
- const value = headers.get("x-request-id") ?? headers.get("x-deepseek-request-id");
1541
- return value === null || value.length === 0 ? void 0 : ProviderRequestId(value);
1542
- }
1543
- /**
1544
- * Map an HTTP status to a stable LlmError code.
1545
- * @param status - status of a non-2xx provider response.
1546
- * @param error - parsed provider error body, when available.
1547
- * @returns the normalized harness error code.
1548
- */
1549
- function httpErrorCode(status, error) {
1550
- if (status === 401 || status === 403) return "AUTH";
1551
- if (status === 413) return "INVALID_REQUEST";
1552
- const detail = [
1553
- error?.code,
1554
- error?.type,
1555
- error?.message
1556
- ].filter(Boolean).join(" ");
1557
- if (isQuotaExceededError(detail)) return QUOTA_EXCEEDED_CODE;
1558
- if (status === 429) return "RATE_LIMIT";
1559
- if (status === 400) {
1560
- if (isContextWindowExceededError(detail)) return CONTEXT_WINDOW_EXCEEDED_CODE;
1561
- return "INVALID_REQUEST";
1562
- }
1563
- if (status >= 500) return "SERVER";
1564
- return `HTTP_${status}`;
1565
- }
1566
- /**
1567
- * The first real `LlmAdapter`. One instance serves every model name it was
1568
- * registered under (the harness model name IS the wire model name).
1569
- *
1570
- * One stable signal reaches both initial fetch and body reads. Caller aborts
1571
- * map to `ABORTED`; the configured per-read idle watchdog maps to `TIMEOUT`.
1572
- */
1828
+ /** DeepSeek provider using Messages content and native thinking replay. */
1573
1829
  var DeepSeekAdapter = class extends LlmAdapter {
1574
- config;
1830
+ dependencies;
1575
1831
  files;
1576
- constructor(config) {
1832
+ imageAccess = (ref) => {
1833
+ const attachments = this.dependencies.resolveAttachments?.();
1834
+ return attachments === void 0 ? void 0 : this.dependencies.resolveImageAccess?.(attachments, ref);
1835
+ };
1836
+ constructor(dependencies) {
1577
1837
  super();
1578
- this.config = config;
1579
- this.files = config.resolveFiles?.() ?? new DeepSeekFileStore();
1838
+ this.dependencies = dependencies;
1839
+ this.files = dependencies.resolveFiles?.() ?? new DeepSeekFileStore();
1580
1840
  }
1581
1841
  providerInfo(provider) {
1582
1842
  return {
@@ -1585,92 +1845,56 @@ var DeepSeekAdapter = class extends LlmAdapter {
1585
1845
  };
1586
1846
  }
1587
1847
  providerRetryPolicy(_provider) {
1588
- return this.config.options().retryPolicy;
1589
- }
1590
- imageRequestPricing(_provider, model) {
1591
- const attachments = this.config.resolveAttachments?.();
1592
- const resolveAccess = attachments === void 0 ? void 0 : (ref) => this.config.resolveImageAccess?.(attachments, ref);
1593
- return deepSeekImageRequestPricing(this.config.options(), model, resolveAccess);
1848
+ return this.dependencies.options().retryPolicy;
1594
1849
  }
1595
1850
  listModels(provider) {
1596
- return Promise.resolve(this.config.options().models.map((model) => modelInfo(provider, model)));
1851
+ const connection = this.dependencies.options();
1852
+ return Promise.resolve(connection.models.map((model) => catalogModelInfo(provider, model)));
1597
1853
  }
1598
1854
  resolveModel(provider, model, _signal) {
1599
- return Promise.resolve(this.modelInfoFor(this.config.options(), provider, model));
1855
+ return Promise.resolve(modelInfo(this.dependencies.options(), provider, model));
1600
1856
  }
1601
- modelInfoFor(connection, provider, model) {
1602
- const configured = connection.models.find((entry) => entry.id === model);
1603
- const contextWindow = configured?.contextWindow ?? connection.defaultContextWindow;
1604
- return {
1605
- ...configured === void 0 ? {
1606
- provider,
1607
- id: model,
1608
- name: model,
1609
- inputModalities: ["text"]
1610
- } : modelInfo(provider, configured),
1611
- context: { contextWindow },
1612
- defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens,
1613
- ...configured?.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: configured.systemPromptUpdate },
1614
- ...connection.defaults.thinking === "disabled" ? { reasoning: {
1615
- efforts: OFF_ONLY_REASONING_EFFORTS,
1616
- defaultEffort: OFF_REASONING_EFFORT
1617
- } } : { reasoning: {
1618
- efforts: REASONING_EFFORTS,
1619
- 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
1620
- } }
1621
- };
1857
+ imageRequestPricing(_provider, model) {
1858
+ return deepSeekImageRequestPricing(this.dependencies.options(), model, this.imageAccess);
1622
1859
  }
1623
1860
  prepareCall(provider, model, _signal) {
1624
- const connection = this.config.options();
1861
+ const connection = this.dependencies.options();
1625
1862
  return Promise.resolve({
1626
- model: this.modelInfoFor(connection, provider, model),
1627
- stream: (options) => this.streamWithConnection(options, connection)
1863
+ model: modelInfo(connection, provider, model),
1864
+ stream: (options) => this.generate(options, connection)
1628
1865
  });
1629
1866
  }
1630
1867
  stream(options) {
1631
- return this.streamWithConnection(options, this.config.options());
1868
+ return this.generate(options, this.dependencies.options());
1632
1869
  }
1633
- async *streamWithConnection(options, connection) {
1870
+ async *generate(options, connection) {
1634
1871
  const env_1 = {
1635
1872
  stack: [],
1636
1873
  error: void 0,
1637
1874
  hasError: false
1638
1875
  };
1639
1876
  try {
1640
- const hasImages = options.messages.some((message) => contentHasImage(message.content));
1641
- let attachments;
1642
- if (hasImages) {
1643
- if (connection.models.find((entry) => entry.id === options.model)?.inputModalities?.includes("image") !== true) throw new LlmError(`DeepSeek model "${options.model}" does not accept image input.`, "UNSUPPORTED_CONTENT");
1644
- attachments = this.config.resolveAttachments?.();
1645
- if (attachments === void 0) throw new LlmError("DeepSeek image conversion requires the durable attachment service.", "UNSUPPORTED_CONTENT");
1646
- }
1647
- const apiKey = await this.config.resolveApiKey(connection);
1648
- const userId = this.config.resolveUserId();
1649
1877
  const consumer = new AbortController();
1650
- const watchdog = __addDisposableResource(env_1, idleWatchdog(options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]), connection.streamIdleTimeoutMs, STREAM_IDLE_TIMEOUT_CODE), false);
1651
- const iterator = this.request(options, watchdog.signal, connection, apiKey, userId, attachments, () => {
1878
+ const watchdog = __addDisposableResource(env_1, idleWatchdog(options.signal === void 0 ? consumer.signal : AbortSignal.any([consumer.signal, options.signal]), connection.streamIdleTimeoutMs, "MESSAGES_IDLE"), false);
1879
+ const iterator = this.request(options, connection, watchdog.signal, () => {
1652
1880
  watchdog.pulse();
1653
- })[Symbol.asyncIterator]();
1654
- let exhausted = false;
1881
+ });
1655
1882
  try {
1656
1883
  while (true) {
1657
- const result = await watchdog.next(iterator);
1658
- if (result.done) {
1659
- exhausted = true;
1660
- return;
1661
- }
1662
- yield result.value;
1884
+ const next = await watchdog.next(iterator);
1885
+ if (next.done) return;
1886
+ yield next.value;
1663
1887
  }
1664
1888
  } catch (error) {
1665
- if (timeoutOf(watchdog.signal, STREAM_IDLE_TIMEOUT_CODE) !== void 0) throw new LlmError(`DeepSeek stream idle timeout after ${connection.streamIdleTimeoutMs}ms`, "TIMEOUT", { cause: error });
1666
- if (options.signal?.aborted) throw new LlmError("DeepSeek request aborted by caller", "ABORTED", { cause: error });
1889
+ if (timeoutOf(watchdog.signal, "MESSAGES_IDLE") !== void 0) throw new LlmError("DeepSeek Messages stream idle timeout", "TIMEOUT", { cause: error });
1890
+ if (options.signal?.aborted) throw new LlmError("DeepSeek Messages request aborted", "ABORTED", { cause: error });
1667
1891
  if (error instanceof LlmError) throw error;
1668
- throw new LlmError(`DeepSeek API stream from ${connection.baseURL} failed`, "TRANSPORT", { cause: error });
1892
+ throw new LlmError("DeepSeek Messages transport failed", "TRANSPORT", { cause: error });
1669
1893
  } finally {
1670
- consumer.abort("DeepSeek stream consumer stopped");
1671
- if (!exhausted && iterator.return !== void 0) try {
1672
- await iterator.return();
1673
- } catch (_abortedTransportTeardown) {}
1894
+ consumer.abort();
1895
+ try {
1896
+ await iterator.return(void 0);
1897
+ } catch (_abortedRequestCleanup) {}
1674
1898
  }
1675
1899
  } catch (e_1) {
1676
1900
  env_1.error = e_1;
@@ -1679,220 +1903,130 @@ var DeepSeekAdapter = class extends LlmAdapter {
1679
1903
  __disposeResources(env_1);
1680
1904
  }
1681
1905
  }
1682
- async *request(options, signal, connection, apiKey, userId, attachments, onActivity) {
1683
- const headers = {
1684
- "authorization": `Bearer ${apiKey}`,
1685
- "content-type": "application/json",
1686
- "accept": "text/event-stream",
1687
- ...attributionHeaders(),
1688
- "x-deepseek-harness-user-id": String(userId),
1689
- ...options.sessionId !== void 0 ? { "x-deepseek-harness-session-id": String(options.sessionId) } : {},
1690
- ...options.purpose === "compaction" ? { "x-deepseek-harness-compact": "1" } : {}
1691
- };
1692
- const fileConnection = {
1906
+ async *request(options, connection, signal, activity) {
1907
+ signal.throwIfAborted();
1908
+ const { messages, versions } = await prepareImages(options.messages, connection, options.model, this.dependencies.resolveAttachments?.(), this.imageAccess, signal);
1909
+ const accountToken = await this.dependencies.resolveAccountToken?.(connection);
1910
+ const key = accountToken ?? await this.dependencies.resolveApiKey(connection);
1911
+ const files = new RequestFiles(this.files, {
1693
1912
  baseURL: connection.baseURL,
1694
- apiKey
1695
- };
1696
- const model = connection.models.find((entry) => entry.id === options.model);
1697
- const policy = model === void 0 ? void 0 : resolveRequestImagePolicy(model);
1698
- const resolveImageAccess = attachments === void 0 ? void 0 : (ref) => this.config.resolveImageAccess?.(attachments, ref);
1699
- const imageAccessOptions = resolveImageAccess === void 0 ? {} : { resolveImageAccess };
1700
- const requestMessages = policy === void 0 ? options.messages : offloadRequestImagesWithPolicy(options.messages, {
1701
- representation: "raw",
1702
- maxBytes: connection.maxRequestFilesBytes,
1703
- maxImages: connection.maxImagesPerRequest,
1704
- byteQuantum: connection.imageOffloadByteQuantum,
1705
- countQuantum: connection.imageOffloadCountQuantum,
1706
- byteLength: (ref) => Math.min(ref.bytes, policy.maxBytes),
1707
- placeholder: (ref) => offloadedImageText(ref, resolveImageAccess?.(ref))
1708
- });
1709
- const requestOptions = requestMessages === options.messages ? options : {
1710
- ...options,
1711
- messages: [...requestMessages]
1712
- };
1713
- const requestImages = attachments === void 0 || model === void 0 ? /* @__PURE__ */ new Map() : await prepareRequestImages(requestOptions, attachments, model, signal);
1714
- let representation = "file";
1715
- let fileAttempt = 0;
1913
+ apiKey: key,
1914
+ accountCredential: accountToken !== void 0
1915
+ }, connection.filePolicy, connection.filesApiTimeoutMs, signal, activity);
1916
+ let inline = false;
1716
1917
  while (true) {
1717
- const usedFiles = [];
1718
- let body;
1719
- if (attachments === void 0) body = serializeRequest(requestOptions, connection.defaults);
1720
- else if (representation === "base64") body = await serializeRequestWithImages(requestOptions, {
1721
- representation: { kind: "base64" },
1722
- requestImages,
1723
- ...imageAccessOptions,
1724
- maxRequestImageBytes: connection.maxInlineRequestImageBytes,
1725
- maxImagesPerRequest: connection.maxImagesPerRequest,
1726
- byteQuantum: connection.inlineImageOffloadByteQuantum,
1727
- countQuantum: connection.imageOffloadCountQuantum
1728
- }, connection.defaults);
1729
- else try {
1730
- body = await serializeRequestWithImages(requestOptions, {
1731
- representation: {
1732
- kind: "file",
1733
- resolveFileId: async (version, _block, location) => {
1734
- const env_2 = {
1735
- stack: [],
1736
- error: void 0,
1737
- hasError: false
1738
- };
1739
- try {
1740
- const filesDeadline = __addDisposableResource(env_2, deadline(signal, connection.filesApiTimeoutMs, FILES_API_TIMEOUT_CODE), false);
1741
- let resolved;
1742
- try {
1743
- resolved = await this.files.ensureUploaded(version, fileConnection, connection.filePolicy, filesDeadline.signal);
1744
- } catch (error) {
1745
- if (signal.aborted) throw error;
1746
- throw new FileResolutionFailure(error);
1747
- }
1748
- onActivity();
1749
- usedFiles.push({
1750
- version,
1751
- fileId: resolved.record.fileId,
1752
- location
1753
- });
1754
- return resolved.record.fileId;
1755
- } catch (e_2) {
1756
- env_2.error = e_2;
1757
- env_2.hasError = true;
1758
- } finally {
1759
- __disposeResources(env_2);
1760
- }
1761
- }
1762
- },
1763
- requestImages,
1764
- ...imageAccessOptions,
1765
- maxRequestImageBytes: connection.maxRequestFilesBytes,
1766
- maxImagesPerRequest: connection.maxImagesPerRequest,
1767
- byteQuantum: connection.imageOffloadByteQuantum,
1768
- countQuantum: connection.imageOffloadCountQuantum
1769
- }, connection.defaults);
1918
+ signal.throwIfAborted();
1919
+ files.beginAttempt();
1920
+ let fileIds;
1921
+ if (!inline) try {
1922
+ fileIds = await prepareFileIds(messages, versions, files);
1770
1923
  } catch (error) {
1771
1924
  if (!(error instanceof FileResolutionFailure)) throw error;
1772
- representation = "base64";
1925
+ inline = true;
1773
1926
  continue;
1774
1927
  }
1775
- let extensions;
1776
- try {
1777
- extensions = await this.config.prepareExtensions({
1778
- body,
1779
- signal,
1780
- ...options.sessionId === void 0 ? {} : { sessionId: String(options.sessionId) },
1781
- ...options.purpose === void 0 ? {} : { purpose: options.purpose }
1928
+ const extensions = await prepareRequestExtensions(serialize(options, connection, inline ? inlineImages(messages, versions, connection) : messages, versions, this.imageAccess, (reason) => {
1929
+ this.dependencies.onReplayDegrade?.({
1930
+ provider: options.provider,
1931
+ model: options.model,
1932
+ reason
1782
1933
  });
1783
- } catch (error) {
1784
- throw new LlmError("DeepSeek request extension preparation failed", "REQUEST_EXTENSION", { cause: error });
1785
- }
1786
- for (const field of Object.keys(extensions.fields)) if (Object.hasOwn(body, field)) throw new LlmError(`DeepSeek request extension field ${JSON.stringify(field)} collides with the base request`, "REQUEST_EXTENSION");
1787
- const payload = JSON.stringify({
1788
- ...body,
1789
- ...extensions.fields
1934
+ }, fileIds), {
1935
+ signal,
1936
+ ...options.sessionId === void 0 ? {} : { sessionId: String(options.sessionId) },
1937
+ ...options.purpose === void 0 ? {} : { purpose: options.purpose }
1938
+ }, this.dependencies.prepareExtensions);
1939
+ signal.throwIfAborted();
1940
+ const response = await fetch(`${messagesApiRoot(connection.baseURL)}/messages`, {
1941
+ method: "POST",
1942
+ signal,
1943
+ body: extensions.payload,
1944
+ redirect: "error",
1945
+ headers: {
1946
+ ...attributionHeaders(),
1947
+ "content-type": "application/json",
1948
+ "accept": "text/event-stream",
1949
+ ...accountToken === void 0 ? { "x-api-key": key } : { "x-dsh-auth-token": accountToken },
1950
+ "anthropic-version": "2023-06-01",
1951
+ ...fileIds === void 0 || fileIds.size === 0 ? {} : { "anthropic-beta": MESSAGES_FILES_BETA },
1952
+ "x-deepseek-harness-user-id": this.dependencies.resolveUserId(),
1953
+ ...options.sessionId === void 0 ? {} : { "x-deepseek-harness-session-id": String(options.sessionId) },
1954
+ ...options.purpose === "compaction" ? { "x-deepseek-harness-compact": "1" } : {}
1955
+ }
1790
1956
  });
1791
- let response;
1792
- try {
1793
- response = await fetch(`${connection.baseURL}/chat/completions`, {
1794
- method: "POST",
1795
- headers,
1796
- body: payload,
1797
- signal
1798
- });
1799
- } catch (error) {
1800
- if (signal.aborted) throw error;
1801
- throw new LlmError(`DeepSeek API request to ${connection.baseURL} failed`, "TRANSPORT", { cause: error });
1802
- }
1803
1957
  if (!response.ok) {
1804
- let message = `DeepSeek API error (HTTP ${response.status})`;
1805
- let providerError;
1806
- const rawResponse = await response.text();
1958
+ const text = await response.text();
1959
+ let raw;
1807
1960
  try {
1808
- providerError = JSON.parse(rawResponse).error;
1809
- if (providerError?.message) message = providerError.message;
1810
- } catch {}
1811
- const detail = [
1812
- providerError?.code,
1813
- providerError?.type,
1814
- providerError?.message
1815
- ].filter((field) => typeof field === "string").join(" ");
1816
- if (usedFiles.length > 0 && providerRejectedFileId(detail)) {
1817
- await Promise.all(staleMappings(usedFiles, detail).map((file) => this.files.invalidate(file.version, file.fileId, fileConnection)));
1818
- if (fileAttempt === 0) {
1819
- fileAttempt += 1;
1820
- continue;
1821
- }
1822
- }
1823
- if (response.status === 400 && usedFiles.length > 0 && providerRejectedNormalizedImage(detail)) message = normalizedImageDiagnostic(usedFiles, message, detail);
1824
- const delay = providerRetryAfterMs(response.headers.get("retry-after"));
1825
- const id = requestId(response.headers);
1826
- throw new LlmError(message, httpErrorCode(response.status, providerError), {
1827
- cause: new Error(rawResponse.length > 0 ? rawResponse : `DeepSeek HTTP ${response.status}`),
1828
- status: response.status,
1829
- ...delay === void 0 ? {} : { providerRetryAfterMs: delay },
1830
- ...id === void 0 ? {} : { requestId: id }
1961
+ raw = JSON.parse(text);
1962
+ } catch (_nonJsonGatewayError) {}
1963
+ const detail = providerErrorDetail(raw);
1964
+ if (await files.retry(detail)) continue;
1965
+ const failure = providerError(raw, response.status, response.headers);
1966
+ throw new LlmError(files.errorMessage(response.status, failure.message, detail), failure.code, {
1967
+ ...failure.failure,
1968
+ cause: new Error(text)
1831
1969
  });
1832
1970
  }
1833
- try {
1834
- await extensions.accept();
1835
- } catch (error) {
1836
- throw new LlmError("DeepSeek request extension acceptance failed", "REQUEST_EXTENSION", { cause: error });
1837
- }
1838
- if (!response.body) throw new LlmError("DeepSeek API returned no response body", "EMPTY_RESPONSE");
1839
- yield* translate(parseSse(response.body, onActivity));
1971
+ await extensions.accept();
1972
+ if (response.body === null) throw new LlmError("DeepSeek Messages returned no response body", "EMPTY_RESPONSE");
1973
+ yield* translate(parseSse(response.body, activity), options.model);
1840
1974
  return;
1841
1975
  }
1842
1976
  }
1843
1977
  };
1844
1978
  //#endregion
1845
- //#region lib/types/index.js
1846
- /**
1847
- * Register a {@link DeepSeekAdapter} for the `deepseek-official` provider route on
1848
- * `ctx.llm`, with connection facts resolved per request instead of frozen at
1849
- * load: the plugin layers its `cordis.yml` entry config under the optional
1850
- * `llm-deepseek` user-settings section (`ctx.settings`) and resolves the API
1851
- * key through the optional credential seam (`ctx.credentials`), so a changed
1852
- * base URL, catalog, or key reaches the very next request without restarting
1853
- * anything, while an in-flight stream keeps the facts it started with. The
1854
- * one registration-captured fact the retry policy — re-registers the route
1855
- * in place when it changes.
1856
- * @module @deepseek-ai/dsh-llm-deepseek
1857
- */
1858
- const name = "llm-deepseek";
1859
- const inject = ["llm"];
1860
- const NS = "llm-deepseek";
1979
+ //#region lib/types/defaults.js
1980
+ /** Shared provider limits and Chat Files API defaults. */
1981
+ /** Default maximum idle interval while an adapter stream read is outstanding. */
1982
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
1983
+ /** Default combined request/response context capacity. */
1984
+ const DEFAULT_CONTEXT_WINDOW = 1e6;
1985
+ /** Default per-request output-token cap. */
1986
+ const DEFAULT_MAX_TOKENS = 256e3;
1987
+ /** Default bound on accumulated base64 image payload after Files API fallback. */
1988
+ const DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
1989
+ /** Deterministic raw-byte removal step. */
1990
+ const DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM = 64 * 1024 * 1024;
1991
+ /** Deterministic base64-byte removal step after Files API fallback. */
1992
+ const DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM = 10 * 1024 * 1024;
1993
+ /** Deterministic image-count removal step. */
1994
+ const DEFAULT_IMAGE_OFFLOAD_COUNT_QUANTUM = 20;
1995
+ /** Default explicit lifetime for uploaded images. */
1996
+ const DEFAULT_FILE_EXPIRY_SECONDS = 10080 * 60;
1997
+ /** Default proactive refresh window for indexed file ids. */
1998
+ const DEFAULT_FILE_REFRESH_MARGIN_SECONDS = 3600;
1999
+ /** Default number of oldest harness-owned files removed on quota recovery. */
2000
+ const DEFAULT_FILE_QUOTA_CLEANUP_BATCH = 100;
2001
+ /** Default deadline for resolving one request image through the Files API. */
2002
+ const DEFAULT_FILES_API_TIMEOUT_MS = 6e4;
2003
+ //#endregion
2004
+ //#region lib/types/models.js
2005
+ /** Default DeepSeek model catalog. */
2006
+ /** Advisory official model entries; deployments may replace the catalog. */
2007
+ const DEFAULT_MODELS = [{
2008
+ id: "deepseek-flash",
2009
+ name: "DeepSeek-V41-Flash",
2010
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
2011
+ inputModalities: ["text", "image"],
2012
+ systemPromptUpdate: "in-history"
2013
+ }, {
2014
+ id: "deepseek-v4-pro",
2015
+ name: "DeepSeek-V4-Pro",
2016
+ description: "Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.",
2017
+ contextWindow: DEFAULT_CONTEXT_WINDOW
2018
+ }];
2019
+ //#endregion
2020
+ //#region lib/types/config.js
1861
2021
  const DEFAULT_API_KEY_ENV = "DEEPSEEK_API_KEY";
1862
- /** The single provider route this plugin owns. */
1863
- const PROVIDER = "deepseek-official";
1864
- const DEFAULT_MODELS = [
1865
- {
1866
- id: "deepseek-flash",
1867
- name: "DeepSeek-V41-Flash",
1868
- contextWindow: DEFAULT_CONTEXT_WINDOW,
1869
- inputModalities: ["text", "image"],
1870
- imagePixelBudget: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
1871
- imageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES,
1872
- systemPromptUpdate: "in-history"
1873
- },
1874
- {
1875
- id: "deepseek-v4-flash",
1876
- name: "DeepSeek-V4-Flash",
1877
- description: "Fast, efficient, and economical; suited to focused, routine, or parallel tasks.",
1878
- contextWindow: DEFAULT_CONTEXT_WINDOW
1879
- },
1880
- {
1881
- id: "deepseek-v4-pro",
1882
- name: "DeepSeek-V4-Pro",
1883
- description: "Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.",
1884
- contextWindow: DEFAULT_CONTEXT_WINDOW
1885
- },
1886
- {
1887
- id: "deepseek-v4-flash-vision-exp",
1888
- name: "DeepSeek-V4-Flash-Vision-Exp",
1889
- contextWindow: DEFAULT_CONTEXT_WINDOW,
1890
- inputModalities: ["text", "image"],
1891
- imagePixelBudget: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
1892
- imageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES
1893
- }
1894
- ];
1895
2022
  const MODEL_MODALITIES = ["text", "image"];
2023
+ /** Read the current value behind every reference of a validated Config.
2024
+ * @param config Parsed plugin Config.
2025
+ * @returns Plain options for the resolver.
2026
+ */
2027
+ function plainOptions(config) {
2028
+ return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, isVolatile(value) ? value.get() : value]));
2029
+ }
1896
2030
  const catalogModel = z.object({
1897
2031
  id: z.string().required(),
1898
2032
  name: z.string(),
@@ -1905,33 +2039,33 @@ const catalogModel = z.object({
1905
2039
  systemPromptUpdate: z.const("in-history")
1906
2040
  });
1907
2041
  const Config = z.object({
1908
- apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
1909
- baseURL: z.string(),
1910
- thinking: z.union(["enabled", "disabled"]),
2042
+ apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV).volatile(),
2043
+ baseURL: z.string().volatile(),
2044
+ thinking: z.union(["enabled", "disabled"]).volatile(),
1911
2045
  reasoningEffort: z.union([
1912
2046
  "off",
1913
2047
  "low",
1914
2048
  "high",
1915
2049
  "max"
1916
- ]),
1917
- maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS),
1918
- defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
1919
- models: z.array(catalogModel).default(DEFAULT_MODELS),
1920
- streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
1921
- maxRequestFilesBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_FILES_BYTES),
1922
- maxInlineRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES),
1923
- maxImagesPerRequest: z.number().step(1).min(1).default(600),
1924
- imageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM),
1925
- inlineImageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM),
1926
- imageOffloadCountQuantum: z.number().step(1).min(1).default(20),
1927
- filesApiTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_FILES_API_TIMEOUT_MS),
1928
- fileExpiresAfterSeconds: z.number().step(1).min(3600).max(2592e3).default(DEFAULT_FILE_EXPIRY_SECONDS),
1929
- fileRefreshMarginSeconds: z.number().step(1).min(0).default(DEFAULT_FILE_REFRESH_MARGIN_SECONDS),
1930
- fileQuotaCleanupBatch: z.number().step(1).min(1).max(1e3).default(100),
1931
- retryPolicy: RetryPolicySchema
2050
+ ]).volatile(),
2051
+ maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS).volatile(),
2052
+ defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW).volatile(),
2053
+ models: z.array(catalogModel).default(DEFAULT_MODELS).volatile(),
2054
+ streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS).volatile(),
2055
+ maxRequestFilesBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_FILES_BYTES).volatile(),
2056
+ maxInlineRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_INLINE_REQUEST_IMAGE_BYTES).volatile(),
2057
+ maxImagesPerRequest: z.number().step(1).min(1).default(600).volatile(),
2058
+ imageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_IMAGE_OFFLOAD_BYTE_QUANTUM).volatile(),
2059
+ inlineImageOffloadByteQuantum: z.number().step(1).min(1).default(DEFAULT_INLINE_IMAGE_OFFLOAD_BYTE_QUANTUM).volatile(),
2060
+ imageOffloadCountQuantum: z.number().step(1).min(1).default(20).volatile(),
2061
+ filesApiTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_FILES_API_TIMEOUT_MS).volatile(),
2062
+ fileExpiresAfterSeconds: z.number().step(1).min(3600).max(2592e3).default(DEFAULT_FILE_EXPIRY_SECONDS).volatile(),
2063
+ fileRefreshMarginSeconds: z.number().step(1).min(0).default(DEFAULT_FILE_REFRESH_MARGIN_SECONDS).volatile(),
2064
+ fileQuotaCleanupBatch: z.number().step(1).min(1).max(1e3).default(100).volatile(),
2065
+ retryPolicy: RetryPolicySchema.volatile()
1932
2066
  });
1933
2067
  /** Public API default; the internal endpoint comes from $DEEPSEEK_BASE_URL. */
1934
- const PUBLIC_BASE_URL = "https://api.deepseek.com";
2068
+ const PUBLIC_BASE_URL = "https://api.deepseek.com/anthropic";
1935
2069
  /** Environment variable naming this provider's endpoint, honored only from trusted layers. */
1936
2070
  const BASE_URL_ENV = "DEEPSEEK_BASE_URL";
1937
2071
  /** Resolve, validate, and detach the advisory model catalog. */
@@ -1964,8 +2098,8 @@ function resolveModels(models) {
1964
2098
  ...model.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: model.systemPromptUpdate },
1965
2099
  inputModalities: [...inputModalities],
1966
2100
  ...hasImage ? {
1967
- imagePixelBudget: model.imagePixelBudget === "low" ? DEFAULT_LOW_DETAIL_IMAGE_PIXEL_BUDGET : model.imagePixelBudget ?? 64e4,
1968
- imageMaxBytes: model.imageMaxBytes ?? 1048576
2101
+ ...model.imagePixelBudget === void 0 ? {} : { imagePixelBudget: model.imagePixelBudget },
2102
+ imageMaxBytes: model.imageMaxBytes ?? 2097152
1969
2103
  } : {}
1970
2104
  };
1971
2105
  });
@@ -1983,6 +2117,7 @@ function resolveModels(models) {
1983
2117
  * @returns validated connection facts plus the credential reference.
1984
2118
  */
1985
2119
  function resolveAdapterOptions(config, environment) {
2120
+ if (Object.hasOwn(config, "protocol")) throw new Error("llm-deepseek: protocol is not configurable; remove it and use a Messages-compatible baseURL");
1986
2121
  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");
1987
2122
  if (config.defaultContextWindow !== void 0 && (!Number.isInteger(config.defaultContextWindow) || config.defaultContextWindow <= 0)) throw new Error("llm-deepseek: defaultContextWindow must be a positive integer");
1988
2123
  if (config.maxTokens !== void 0 && (!Number.isSafeInteger(config.maxTokens) || config.maxTokens <= 0)) throw new Error("llm-deepseek: maxTokens must be a positive safe integer");
@@ -2011,9 +2146,12 @@ function resolveAdapterOptions(config, environment) {
2011
2146
  if (!Number.isSafeInteger(fileRefreshMarginSeconds) || fileRefreshMarginSeconds < 0 || fileRefreshMarginSeconds >= fileExpiresAfterSeconds) throw new Error("llm-deepseek: fileRefreshMarginSeconds must be a non-negative integer below fileExpiresAfterSeconds");
2012
2147
  const fileQuotaCleanupBatch = config.fileQuotaCleanupBatch ?? 100;
2013
2148
  if (!Number.isSafeInteger(fileQuotaCleanupBatch) || fileQuotaCleanupBatch < 1 || fileQuotaCleanupBatch > 1e3) throw new Error("llm-deepseek: fileQuotaCleanupBatch must be an integer from 1 through 1000");
2149
+ const baseURL = config.baseURL ?? environment?.get(BASE_URL_ENV)?.value ?? "https://api.deepseek.com/anthropic";
2150
+ const parsed = new URL(baseURL);
2151
+ 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");
2014
2152
  return {
2015
2153
  apiKeyEnv: credentialRef(config.apiKeyEnv ?? DEFAULT_API_KEY_ENV),
2016
- baseURL: config.baseURL ?? environment?.get(BASE_URL_ENV)?.value ?? "https://api.deepseek.com",
2154
+ baseURL,
2017
2155
  defaults: {
2018
2156
  thinking: config.thinking,
2019
2157
  reasoningEffort: config.reasoningEffort
@@ -2037,26 +2175,17 @@ function resolveAdapterOptions(config, environment) {
2037
2175
  retryPolicy: resolveRetryPolicy(config.retryPolicy, "llm-deepseek: retryPolicy")
2038
2176
  };
2039
2177
  }
2178
+ //#endregion
2179
+ //#region lib/types/index.js
2180
+ const name = "llm-deepseek";
2181
+ const inject = ["llm"];
2182
+ const NS = "llm-deepseek";
2183
+ const PROVIDER = "deepseek-official";
2040
2184
  function apply(ctx, config) {
2041
- let current = () => config;
2042
- let lastRaw;
2043
- let lastGood;
2044
- const options = () => {
2045
- const raw = current();
2046
- if (raw === lastRaw && lastGood !== void 0) return lastGood;
2047
- try {
2048
- const next = resolveAdapterOptions(raw, launchEnvironmentOf(ctx));
2049
- lastRaw = raw;
2050
- lastGood = next;
2051
- return next;
2052
- } catch (error) {
2053
- if (lastGood === void 0) throw error;
2054
- lastRaw = raw;
2055
- ctx.logger.error("llm-deepseek: keeping the last good configuration after an invalid settings section");
2056
- ctx.logger.error(error);
2057
- return lastGood;
2058
- }
2059
- };
2185
+ ctx.inject(["settings"], (child) => {
2186
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
2187
+ });
2188
+ const options = () => resolveAdapterOptions(plainOptions(config), launchEnvironmentOf(ctx));
2060
2189
  options();
2061
2190
  const resolveApiKey = async (connection) => {
2062
2191
  const ref = connection.apiKeyEnv;
@@ -2074,7 +2203,11 @@ function apply(ctx, config) {
2074
2203
  const resolveUserId = () => userId ??= getOrCreateAnonymousUserId();
2075
2204
  const adapter = new DeepSeekAdapter({
2076
2205
  options,
2206
+ onReplayDegrade: ({ provider, model, reason }) => {
2207
+ ctx.logger.warn(`llm-deepseek: unusable Messages replay state on assistant history for route "${provider}/${model}"; sending provider-neutral content (${reason})`);
2208
+ },
2077
2209
  resolveApiKey,
2210
+ resolveAccountToken: (connection) => ctx.get("deepseekAccount")?.resolveToken(connection.baseURL) ?? Promise.resolve(void 0),
2078
2211
  resolveUserId,
2079
2212
  resolveAttachments: () => ctx.get("attachments"),
2080
2213
  resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
@@ -2088,25 +2221,24 @@ function apply(ctx, config) {
2088
2221
  ctx.llm.registerConfigurableProviders([{
2089
2222
  provider: PROVIDER,
2090
2223
  displayName: "DeepSeek",
2091
- settingsNs: NS,
2224
+ settingsNs: ctx.fiber.entry?.options.id ?? NS,
2092
2225
  settingsPath: []
2093
2226
  }]);
2094
2227
  const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
2095
2228
  let registeredPolicy = options().retryPolicy;
2096
2229
  const ensureRegistrationFacts = () => {
2097
- const policy = options().retryPolicy;
2230
+ let policy;
2231
+ try {
2232
+ policy = options().retryPolicy;
2233
+ } catch (error) {
2234
+ ctx.logger.warn(error);
2235
+ return;
2236
+ }
2098
2237
  if (deepEqualJson(policy, registeredPolicy)) return;
2099
2238
  registration.replace([PROVIDER]);
2100
2239
  registeredPolicy = policy;
2101
2240
  };
2102
- ctx.inject(["settings"], (settingsCtx) => {
2103
- settingsCtx.settings.installSection(ctx, NS, Config, config, {
2104
- setSource: (source) => {
2105
- current = source;
2106
- },
2107
- onChange: ensureRegistrationFacts
2108
- });
2109
- });
2241
+ ctx.on("loader/volatile-update", ensureRegistrationFacts);
2110
2242
  }
2111
2243
  //#endregion
2112
- 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_REQUEST_IMAGE_MAX_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DeepSeekAdapter, DeepSeekFileId, DeepSeekFileStore, DeepSeekFilesClient, DeepSeekUploadIndex, MAX_CHAT_IMAGE_BYTES, MAX_FILE_EXPIRY_SECONDS, MAX_FILE_UPLOAD_BYTES, MAX_STORED_FILE_BYTES, MAX_STORED_FILE_COUNT, MIN_FILE_EXPIRY_SECONDS, PUBLIC_BASE_URL, apply, deepSeekFileScope, deepSeekImageRequestPricing, deepSeekImageTokens, inject, name, resolveAdapterOptions, resolveRequestImagePolicy };
2244
+ 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_REQUEST_IMAGE_MAX_BYTES, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DeepSeekAdapter, DeepSeekFileId, DeepSeekFileStore, DeepSeekFilesClient, DeepSeekUploadIndex, MAX_FILE_EXPIRY_SECONDS, MAX_FILE_UPLOAD_BYTES, MAX_IMAGE_BYTES, MAX_STORED_FILE_BYTES, MAX_STORED_FILE_COUNT, MIN_FILE_EXPIRY_SECONDS, PUBLIC_BASE_URL, REQUEST_IMAGE_MAX_DIMENSION, apply, deepSeekFileScope, deepSeekImageRequestPricing, deepSeekImageTokens, deepSeekRequestImageDimensions, inject, name, plainOptions, resolveAdapterOptions, resolveRequestImageMaxBytes, resolveRequestImageTarget };