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