@toddzheng024/dscode-bundle 0.7.27 → 0.7.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +10 -10
- package/bootstrap.mjs +1 -1
- package/cordis.patch.yml +433 -46
- package/package.json +280 -245
- package/plugins/account/index.mjs +178 -0
- package/plugins/account/state.mjs +123 -0
- package/plugins/auto-review/index.mjs +8 -5
- package/plugins/code-review/index.mjs +1 -1
- package/plugins/computer-use/index.mjs +37 -0
- package/plugins/cordis-config/plain.mjs +31 -0
- package/plugins/dscode/index.mjs +1 -1
- package/plugins/exec/cli.mjs +1 -1
- package/plugins/grok/index.mjs +28 -24
- package/plugins/i18n/messages.mjs +204 -0
- package/plugins/memory/index.mjs +1 -1
- package/plugins/message-source/kind.mjs +20 -0
- package/plugins/openrouter/adapter.mjs +29 -13
- package/plugins/openrouter/index.mjs +31 -28
- package/plugins/session-bridge/communication.mjs +6 -3
- package/plugins/session-bridge/server.mjs +3 -2
- package/plugins/session-bridge/tasks.mjs +3 -1
- package/plugins/session-cards/index.mjs +1 -1
- package/plugins/time-marks/index.mjs +3 -2
- package/plugins/time-marks/marks.mjs +6 -5
- package/plugins/triggers/config.mjs +1 -1
- package/plugins/triggers/tools.mjs +1 -1
- package/plugins/tui-tools/doctor.mjs +1 -1
- package/plugins/tui-tools/index.mjs +16 -4
- package/plugins/ultra/policy.mjs +16 -0
- package/vendor/bash/index.js +455 -186
- package/vendor/bash/types/background.d.ts +38 -7
- package/vendor/bash/types/index.d.ts +20 -4
- package/vendor/bash/types/render.d.ts +28 -9
- package/vendor/command-goal/index.js +3 -1
- package/vendor/deepseek/index.js +1378 -1246
- package/vendor/deepseek/types/adapter.d.ts +16 -155
- package/vendor/deepseek/types/config.d.ts +127 -0
- package/vendor/deepseek/types/defaults.d.ts +24 -0
- package/vendor/deepseek/types/file-store.d.ts +5 -3
- package/vendor/deepseek/types/files-api.d.ts +12 -9
- package/vendor/deepseek/types/image-tokens.d.ts +24 -9
- package/vendor/deepseek/types/images.d.ts +35 -0
- package/vendor/deepseek/types/index.d.ts +10 -94
- package/vendor/deepseek/types/messages-api.d.ts +10 -0
- package/vendor/deepseek/types/model-info.d.ts +16 -0
- package/vendor/deepseek/types/models.d.ts +4 -0
- package/vendor/deepseek/types/replay.d.ts +27 -0
- package/vendor/deepseek/types/request-extensions.d.ts +16 -0
- package/vendor/deepseek/types/request-files.d.ts +49 -0
- package/vendor/deepseek/types/request-pricing.d.ts +32 -20
- package/vendor/deepseek/types/serialize.d.ts +17 -82
- package/vendor/deepseek/types/sse.d.ts +6 -22
- package/vendor/deepseek/types/translate.d.ts +13 -36
- package/vendor/deepseek/types/transport.d.ts +15 -0
- package/vendor/deepseek/types/types.d.ts +106 -167
- package/vendor/deepseek/types/wire-types.d.ts +62 -0
- package/vendor/persistent/index.js +9 -3
- package/vendor/subagent/index.js +11 -8
- package/vendor/subagent/model-selection-settings.js +12 -38
- package/vendor/subagent/types/index.d.ts +4 -3
- package/vendor/subagent/types/model-selection-settings.d.ts +12 -10
- package/vendor/subagent-core/index.js +401 -264
- package/vendor/subagent-core/typert.host.js +103 -113
- package/vendor/subagent-core/typert.remote-client.d.ts +1 -3
- package/vendor/subagent-core/typert.remote-client.js +23 -69
- package/vendor/subagent-core/types/archive-admission.d.ts +16 -0
- package/vendor/subagent-core/types/archive-admission.js +151 -0
- package/vendor/subagent-core/types/assistant-output.d.ts +2 -2
- package/vendor/subagent-core/types/catalog.d.ts +17 -10
- package/vendor/subagent-core/types/catalog.js +12 -6
- package/vendor/subagent-core/types/child-agent.d.ts +8 -5
- package/vendor/subagent-core/types/child-agent.js +11 -5
- package/vendor/subagent-core/types/continuation-activation.d.ts +17 -1
- package/vendor/subagent-core/types/continuation-activation.js +41 -4
- package/vendor/subagent-core/types/continuation-messages.d.ts +1 -1
- package/vendor/subagent-core/types/continuation-messages.js +7 -3
- package/vendor/subagent-core/types/continuation.d.ts +1 -1
- package/vendor/subagent-core/types/continuation.js +2 -2
- package/vendor/subagent-core/types/control-types.d.ts +29 -36
- package/vendor/subagent-core/types/control-types.js +2 -3
- package/vendor/subagent-core/types/control.d.ts +2 -28
- package/vendor/subagent-core/types/control.js +3 -40
- package/vendor/subagent-core/types/index.d.ts +45 -41
- package/vendor/subagent-core/types/index.js +46 -82
- package/vendor/subagent-core/types/internal.d.ts +2 -2
- package/vendor/subagent-core/types/internal.js +2 -2
- package/vendor/subagent-core/types/lifecycle.d.ts +1 -1
- package/vendor/subagent-core/types/lifecycle.js +5 -3
- package/vendor/subagent-core/types/list-children.d.ts +15 -32
- package/vendor/subagent-core/types/list-children.js +45 -49
- package/vendor/subagent-core/types/out-of-process.d.ts +1 -1
- package/vendor/subagent-core/types/projection-types.d.ts +9 -1
- package/vendor/subagent-core/types/projection.d.ts +5 -0
- package/vendor/subagent-core/types/projection.js +10 -4
- package/vendor/subagent-core/types/run-settlement.js +1 -1
- package/vendor/subagent-core/types/types.d.ts +2 -2
- package/vendor/terminal/index.js +94 -30
- package/vendor/tui/lib/index.mjs +2 -1
- package/vendor/tui/lib/kernel-panels.mjs +1 -1
- package/vendor/tui/lib/render/projection.mjs +50 -36
- package/presets/dscode/agent.cordis.yml +0 -321
- package/presets/dscode/preset.yml +0 -3
package/vendor/deepseek/index.js
CHANGED
|
@@ -1,542 +1,105 @@
|
|
|
1
|
-
// dscode-ultra-
|
|
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
|
|
4
|
-
if (options.reasoningEffort !== 'ultra' || options.purpose || !options.tools?.some(t => t.name === 'subagent' || t.name === 'subagent_fork')) return
|
|
5
|
-
|
|
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
|
|
13
|
-
if (options.model !== 'deepseek-flash' || options.purpose || !options.sessionId) return
|
|
14
|
-
|
|
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
|
|
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 {
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
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
|
|
66
|
+
function catalogModelInfo(provider, model) {
|
|
484
67
|
return {
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
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
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
605
|
-
createdAt
|
|
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
|
|
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("
|
|
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
|
|
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))
|
|
678
|
-
if (file.expiresAt === void 0) throw invalidResponse("upload");
|
|
270
|
+
}, input.signal), "upload"), "upload");
|
|
679
271
|
return {
|
|
680
272
|
...file,
|
|
681
|
-
expiresAt: file.
|
|
273
|
+
expiresAt: file.createdAt + input.expiresAfterSeconds
|
|
682
274
|
};
|
|
683
275
|
}
|
|
684
276
|
/**
|
|
685
|
-
* List one
|
|
686
|
-
* @param options - pagination
|
|
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(
|
|
691
|
-
if (options.after !== void 0) query.set("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
701
|
-
...typeof
|
|
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))
|
|
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))
|
|
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.
|
|
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
|
-
/**
|
|
894
|
-
const
|
|
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 = `${
|
|
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
|
|
1005
|
-
const scope =
|
|
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
|
|
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(
|
|
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 =
|
|
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 (
|
|
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(
|
|
1096
|
-
|
|
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
|
|
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(
|
|
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/
|
|
1139
|
-
/**
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
function
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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: "
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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: "
|
|
1309
|
-
index:
|
|
1310
|
-
|
|
1730
|
+
type: "block-end",
|
|
1731
|
+
index: block.index,
|
|
1732
|
+
block: { ...block.content }
|
|
1311
1733
|
};
|
|
1312
1734
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
/**
|
|
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
|
-
|
|
1830
|
+
dependencies;
|
|
1575
1831
|
files;
|
|
1576
|
-
|
|
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.
|
|
1579
|
-
this.files =
|
|
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.
|
|
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
|
-
|
|
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(
|
|
1855
|
+
return Promise.resolve(modelInfo(this.dependencies.options(), provider, model));
|
|
1600
1856
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
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.
|
|
1861
|
+
const connection = this.dependencies.options();
|
|
1625
1862
|
return Promise.resolve({
|
|
1626
|
-
model:
|
|
1627
|
-
stream: (options) => this.
|
|
1863
|
+
model: modelInfo(connection, provider, model),
|
|
1864
|
+
stream: (options) => this.generate(options, connection)
|
|
1628
1865
|
});
|
|
1629
1866
|
}
|
|
1630
1867
|
stream(options) {
|
|
1631
|
-
return this.
|
|
1868
|
+
return this.generate(options, this.dependencies.options());
|
|
1632
1869
|
}
|
|
1633
|
-
async *
|
|
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([
|
|
1651
|
-
const iterator = this.request(options, watchdog.signal,
|
|
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
|
-
})
|
|
1654
|
-
let exhausted = false;
|
|
1881
|
+
});
|
|
1655
1882
|
try {
|
|
1656
1883
|
while (true) {
|
|
1657
|
-
const
|
|
1658
|
-
if (
|
|
1659
|
-
|
|
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,
|
|
1666
|
-
if (options.signal?.aborted) throw new LlmError("DeepSeek request aborted
|
|
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(
|
|
1892
|
+
throw new LlmError("DeepSeek Messages transport failed", "TRANSPORT", { cause: error });
|
|
1669
1893
|
} finally {
|
|
1670
|
-
consumer.abort(
|
|
1671
|
-
|
|
1672
|
-
await iterator.return();
|
|
1673
|
-
} catch (
|
|
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,
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
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
|
-
|
|
1697
|
-
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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
|
-
|
|
1925
|
+
inline = true;
|
|
1773
1926
|
continue;
|
|
1774
1927
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
-
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1805
|
-
let
|
|
1806
|
-
const rawResponse = await response.text();
|
|
1958
|
+
const text = await response.text();
|
|
1959
|
+
let raw;
|
|
1807
1960
|
try {
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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/
|
|
1846
|
-
/**
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
*
|
|
1857
|
-
*/
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
const
|
|
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
|
-
|
|
1968
|
-
imageMaxBytes: model.imageMaxBytes ??
|
|
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
|
|
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
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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 };
|