@morlay/dsh-llm-openai-compatible 0.0.9-alpha.5 → 0.0.9-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -36
- package/dist/index.d.mts +20 -5
- package/dist/index.mjs +29 -29
- package/dist/{translate-CwniWJOm.mjs → translate-D9PyJrw2.mjs} +18 -18
- package/dist/wire.mjs +1 -1
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +15 -13
- package/src/index.ts +59 -39
- package/src/serialize.ts +41 -21
package/README.md
CHANGED
|
@@ -15,46 +15,54 @@ DeepSeek Harness 的 **OpenAI 兼容 LLM 适配器**插件。与内置 `llm-pi-a
|
|
|
15
15
|
## 配置
|
|
16
16
|
|
|
17
17
|
`providers` 是 dict:**key 就是 provider 路由键**(选择器与
|
|
18
|
-
`GenerateOptions.provider` 使用),值是 profile
|
|
18
|
+
`GenerateOptions.provider` 使用),值是 profile。它标了 `.volatile()`——**运行期可改**:
|
|
19
|
+
值经 Loader 的引用读取,设置页(Models 页)改的就是它,不需要重挂这行插件(见
|
|
20
|
+
[ADR-运行期改配置走volatile引用](./.agents/adrs/20260922-运行期改配置走volatile引用.md))。
|
|
21
|
+
|
|
22
|
+
配置写在**这行的 config** 里(bundle patch / profile patch / 设置页都落到这里):
|
|
19
23
|
|
|
20
24
|
```yaml
|
|
21
|
-
llm-openai-compatible
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
25
|
+
- id: llm-openai-compatible
|
|
26
|
+
name: "@morlay/dsh-llm-openai-compatible"
|
|
27
|
+
config:
|
|
28
|
+
providers:
|
|
29
|
+
ollama:
|
|
30
|
+
apiKeyEnv: OLLAMA_API_KEY
|
|
31
|
+
baseURL: https://ollama.com/v1
|
|
32
|
+
displayName: Ollama Gateway
|
|
33
|
+
# === 采样默认参数(请求级 temperature 优先)===
|
|
34
|
+
temperature: 1 # 0..2
|
|
35
|
+
topP: 0.95 # 0..1 → wire top_p
|
|
36
|
+
topK: 40 # 正整数 → wire top_k(非标准,仅网关支持时发送)
|
|
37
|
+
presencePenalty: 0 # -2..2 → wire presence_penalty
|
|
38
|
+
frequencyPenalty: 0 # -2..2 → wire frequency_penalty
|
|
39
|
+
seed: 42 # 正整数 → wire seed
|
|
40
|
+
# === 推理 ===
|
|
41
|
+
reasoning: high # 部署默认档位(省略 = 提供方默认)
|
|
42
|
+
# === 模型目录 ===
|
|
43
|
+
defaultContextWindow: 262144
|
|
44
|
+
defaultMaxTokens: 32768
|
|
45
|
+
models:
|
|
46
|
+
- id: deepseek-v4-flash:0731
|
|
47
|
+
name: DeepSeek V4 Flash
|
|
48
|
+
contextWindow: 1000000
|
|
49
|
+
maxTokens: 65535
|
|
50
|
+
inputModalities: [text, image]
|
|
51
|
+
reasoningEfforts:
|
|
52
|
+
off: # off 空值 = 不发送 reasoning_effort
|
|
53
|
+
high: high # 档位 → wire reasoning_effort 拼写
|
|
54
|
+
max: max
|
|
55
|
+
# === 传输 ===
|
|
56
|
+
maxRequestImageBytes: 20971520
|
|
57
|
+
streamIdleTimeoutMs: 300000
|
|
58
|
+
timeoutMs: 600000 # 整体请求超时;缺省不设
|
|
59
|
+
retryPolicy:
|
|
60
|
+
mode: normal
|
|
61
|
+
maxRetries: 5
|
|
56
62
|
```
|
|
57
63
|
|
|
64
|
+
> 旧 `$DSH_HOME/settings.yaml` 的 `llm-openai-compatible` 段由上游启动时一次性导入到同 id 的行 config。
|
|
65
|
+
|
|
58
66
|
## 规则与取舍
|
|
59
67
|
|
|
60
68
|
规则细节(合并表、wire 字段落点、端点与错误映射)在各自 ADR 里维护,这里只列结论:
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { a as OpenAICompatibleAdapter, c as ResolvedModelProfile, i as DEFAULT_STREAM_IDLE_TIMEOUT_MS, l as ResolvedProviderProfile, n as DEFAULT_MAX_REQUEST_IMAGE_BYTES, o as OpenAICompatibleAdapterOptions, r as DEFAULT_MAX_TOKENS, s as ReasoningEffort, t as DEFAULT_CONTEXT_WINDOW } from "./adapter-CYd_pegB.mjs";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { ModelModality, RetryPolicyConfig } from "@deepseek-ai/dsh-llm";
|
|
4
|
-
import { Context } from "@deepseek-ai/cordis";
|
|
4
|
+
import { Context, Volatile } from "@deepseek-ai/cordis";
|
|
5
|
+
//#region ../../../vendor/deepseek-harness/vendor/cosmokit/lib/types/misc.d.ts
|
|
6
|
+
/** String/symbol keyed dictionary type. */
|
|
7
|
+
type Dict<T = any, K extends string | symbol = string> = { [key in K]: T; };
|
|
8
|
+
//#endregion
|
|
5
9
|
//#region src/index.d.ts
|
|
6
10
|
declare const name = "llm-openai-compatible";
|
|
7
11
|
declare const inject: string[];
|
|
@@ -38,12 +42,23 @@ interface ProviderProfileSource {
|
|
|
38
42
|
retryPolicy?: RetryPolicyConfig;
|
|
39
43
|
}
|
|
40
44
|
interface Config {
|
|
41
|
-
|
|
45
|
+
/**
|
|
46
|
+
* provider 路由,key 是路由键。**volatile**:值经 Loader 的引用读取
|
|
47
|
+
* (`config.providers.get()`),设置页改它时不需要重挂这行插件——上游 0.1.7 起
|
|
48
|
+
* 「运行期可改」只有这一条路(旧的 settings namespace section 覆盖已取消)。
|
|
49
|
+
*/
|
|
50
|
+
providers: Volatile<Record<string, ProviderProfileSource>>;
|
|
42
51
|
}
|
|
43
|
-
|
|
52
|
+
/** 解析成普通值之后的配置形状(校验与解析只认它)。 */
|
|
53
|
+
type Options = { [K in keyof Config]?: Config[K] extends Volatile<infer T> ? T : never; };
|
|
54
|
+
declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
55
|
+
providers: z<NoInfer<Dict<ProviderProfileSource, string>>, NoInfer<Dict<ProviderProfileSource, string>>, "volatile-defined">;
|
|
56
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
57
|
+
providers: z<NoInfer<Dict<ProviderProfileSource, string>>, NoInfer<Dict<ProviderProfileSource, string>>, "volatile-defined">;
|
|
58
|
+
}>>, "plain">;
|
|
44
59
|
declare function resolveAdapterOptions(provider: string, source: ProviderProfileSource): ResolvedProviderProfile;
|
|
45
60
|
declare function resolveProfiles(providers: Readonly<Record<string, ProviderProfileSource>> | undefined): Map<string, ResolvedProviderProfile>;
|
|
46
|
-
declare function assertServiceable(config:
|
|
61
|
+
declare function assertServiceable(config: Options): void;
|
|
47
62
|
declare function apply(ctx: Context, config: Config): void;
|
|
48
63
|
//#endregion
|
|
49
|
-
export { Config, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, MODEL_MODALITIES, ModelProfileSource, NS, OpenAICompatibleAdapter, type OpenAICompatibleAdapterOptions, ProviderProfileSource, REASONING_LEVELS, type ReasoningEffort, type ResolvedModelProfile, type ResolvedProviderProfile, apply, assertServiceable, inject, name, resolveAdapterOptions, resolveProfiles };
|
|
64
|
+
export { Config, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, MODEL_MODALITIES, ModelProfileSource, NS, OpenAICompatibleAdapter, type OpenAICompatibleAdapterOptions, Options, ProviderProfileSource, REASONING_LEVELS, type ReasoningEffort, type ResolvedModelProfile, type ResolvedProviderProfile, apply, assertServiceable, inject, name, resolveAdapterOptions, resolveProfiles };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as serializeCallOptions, o as serializeCallOptionsWithImages, r as translate } from "./translate-
|
|
1
|
+
import { a as serializeCallOptions, o as serializeCallOptionsWithImages, r as translate } from "./translate-D9PyJrw2.mjs";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, RetryPolicySchema, assertUsableApiKey, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
@@ -288,7 +288,7 @@ const providerSchema = z.object({
|
|
|
288
288
|
timeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS),
|
|
289
289
|
retryPolicy: RetryPolicySchema
|
|
290
290
|
});
|
|
291
|
-
const Config = z.object({ providers: z.dict(providerSchema).default({}) });
|
|
291
|
+
const Config = z.object({ providers: z.dict(providerSchema).default({}).volatile() });
|
|
292
292
|
function isReasoningEffort(value) {
|
|
293
293
|
return REASONING_LEVELS.includes(value);
|
|
294
294
|
}
|
|
@@ -397,25 +397,25 @@ function registrationFacts(profiles) {
|
|
|
397
397
|
retryPolicy: profile.retryPolicy
|
|
398
398
|
})).sort((left, right) => left.provider.localeCompare(right.provider));
|
|
399
399
|
}
|
|
400
|
-
function directoryEntries(profiles) {
|
|
400
|
+
function directoryEntries(profiles, settingsNs) {
|
|
401
401
|
const entries = /* @__PURE__ */ new Map();
|
|
402
402
|
for (const [provider, profile] of profiles) entries.set(provider, {
|
|
403
403
|
provider,
|
|
404
404
|
displayName: profile.displayName,
|
|
405
|
-
settingsNs
|
|
405
|
+
settingsNs,
|
|
406
406
|
settingsPath: ["providers", provider],
|
|
407
407
|
declared: true
|
|
408
408
|
});
|
|
409
409
|
return [...entries.values()];
|
|
410
410
|
}
|
|
411
411
|
function apply(ctx, config) {
|
|
412
|
-
|
|
412
|
+
const settingsNs = ctx.fiber.entry?.options.id ?? "llm-openai-compatible";
|
|
413
413
|
let lastRaw;
|
|
414
414
|
let memoized;
|
|
415
415
|
const profiles = () => {
|
|
416
|
-
const raw =
|
|
416
|
+
const raw = config.providers.get();
|
|
417
417
|
if (raw === lastRaw && memoized !== void 0) return memoized;
|
|
418
|
-
const next = resolveProfiles(raw
|
|
418
|
+
const next = resolveProfiles(raw);
|
|
419
419
|
lastRaw = raw;
|
|
420
420
|
memoized = next;
|
|
421
421
|
return next;
|
|
@@ -445,7 +445,7 @@ function apply(ctx, config) {
|
|
|
445
445
|
let directory;
|
|
446
446
|
let directoryFacts;
|
|
447
447
|
const ensureDirectory = () => {
|
|
448
|
-
const entries = directoryEntries(profiles());
|
|
448
|
+
const entries = directoryEntries(profiles(), settingsNs);
|
|
449
449
|
if (deepEqualJson(entries, directoryFacts)) return;
|
|
450
450
|
if (directory === void 0) directory = ctx.llm.registerConfigurableProviders(entries);
|
|
451
451
|
else directory.replace(entries);
|
|
@@ -468,27 +468,27 @@ function apply(ctx, config) {
|
|
|
468
468
|
registeredFacts = facts;
|
|
469
469
|
};
|
|
470
470
|
ensureRegistrationFacts();
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
471
|
+
const refresh = () => {
|
|
472
|
+
try {
|
|
473
|
+
ensureRegistrationFacts();
|
|
474
|
+
} catch (error) {
|
|
475
|
+
ctx.logger.error("llm-openai-compatible: keeping the previously registered routes after a refused update");
|
|
476
|
+
ctx.logger.error(error);
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
ensureDirectory();
|
|
480
|
+
} catch (error) {
|
|
481
|
+
ctx.logger.error("llm-openai-compatible: keeping the previous configurable-provider directory after a refused update");
|
|
482
|
+
ctx.logger.error(error);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
ctx.on("loader/volatile-update", refresh);
|
|
486
|
+
ctx.on("internal/config", function(_raw, next) {
|
|
487
|
+
const raw = next();
|
|
488
|
+
if (this !== ctx.fiber) return raw;
|
|
489
|
+
const candidate = Config(raw);
|
|
490
|
+
assertServiceable({ providers: structuredClone(candidate.providers.get()) });
|
|
491
|
+
return raw;
|
|
492
492
|
});
|
|
493
493
|
}
|
|
494
494
|
//#endregion
|
|
@@ -22,7 +22,7 @@ function assertTextOnly(blocks) {
|
|
|
22
22
|
if (contentHasImage(blocks)) throw new LlmError("The OpenAI-compatible chat-completions adapter does not support image content in this message.", "UNSUPPORTED_CONTENT");
|
|
23
23
|
}
|
|
24
24
|
function assertSupportedImageRoles(messages) {
|
|
25
|
-
for (const message of messages) if (message.role !== "user" && contentHasImage(message.content)) throw new LlmError(`The OpenAI-compatible chat-completions adapter cannot represent image content in a ${message.role} message.`, "UNSUPPORTED_CONTENT");
|
|
25
|
+
for (const message of messages) if (message.role !== "user" && message.role !== "tool" && contentHasImage(message.content)) throw new LlmError(`The OpenAI-compatible chat-completions adapter cannot represent image content in a ${message.role} message.`, "UNSUPPORTED_CONTENT");
|
|
26
26
|
}
|
|
27
27
|
function assertRetainedImagesFit(messages, maxRequestImageBytes) {
|
|
28
28
|
const offloadImages = requiredImageOffload(messages, {
|
|
@@ -94,8 +94,6 @@ async function userParts(blocks, resolveImage, signal) {
|
|
|
94
94
|
case "image":
|
|
95
95
|
if (resolveImage === void 0) throw new LlmError("The OpenAI-compatible chat-completions adapter does not support image content in this message.", "UNSUPPORTED_CONTENT");
|
|
96
96
|
parts.push(await resolveImage(block, signal));
|
|
97
|
-
break;
|
|
98
|
-
case "tool-result": parts.push(...await userParts(block.content, resolveImage, signal));
|
|
99
97
|
}
|
|
100
98
|
return parts;
|
|
101
99
|
}
|
|
@@ -117,6 +115,8 @@ async function serializePrompt(messages, resolveImage, signal) {
|
|
|
117
115
|
pendingToolImages = [];
|
|
118
116
|
};
|
|
119
117
|
for (const message of messages) {
|
|
118
|
+
if (message.role === "developer") throw new LlmError("The OpenAI-compatible chat-completions adapter cannot serialize developer messages.", "UNSUPPORTED_CONTENT");
|
|
119
|
+
if (message.content.some((block) => block.type === "tool-addition" || block.type === "tool-removal")) throw new LlmError("The OpenAI-compatible chat-completions adapter cannot serialize tool-change blocks outside developer messages.", "UNSUPPORTED_CONTENT");
|
|
120
120
|
if (message.role === "system") {
|
|
121
121
|
flushToolImages();
|
|
122
122
|
prompt.push({
|
|
@@ -134,34 +134,34 @@ async function serializePrompt(messages, resolveImage, signal) {
|
|
|
134
134
|
});
|
|
135
135
|
continue;
|
|
136
136
|
}
|
|
137
|
-
|
|
138
|
-
const toolResults = message.content.filter((block) => block.type === "tool-result");
|
|
139
|
-
const content = await userParts(regular, resolveImage, signal);
|
|
140
|
-
if (content.length > 0 || toolResults.length === 0) {
|
|
141
|
-
flushToolImages();
|
|
142
|
-
prompt.push({
|
|
143
|
-
role: "user",
|
|
144
|
-
content
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
for (const result of toolResults) {
|
|
137
|
+
if (message.role === "tool") {
|
|
148
138
|
const images = [];
|
|
149
139
|
if (resolveImage !== void 0) {
|
|
150
|
-
for (const block of
|
|
140
|
+
for (const block of message.content) if (block.type === "image") images.push(await resolveImage(block, signal));
|
|
151
141
|
}
|
|
142
|
+
flushToolImages();
|
|
152
143
|
prompt.push({
|
|
153
144
|
role: "tool",
|
|
154
145
|
content: [{
|
|
155
146
|
type: "tool-result",
|
|
156
|
-
toolCallId:
|
|
157
|
-
toolName: toolNames.get(
|
|
147
|
+
toolCallId: message.toolCallId,
|
|
148
|
+
toolName: toolNames.get(message.toolCallId) ?? "",
|
|
158
149
|
output: {
|
|
159
150
|
type: "text",
|
|
160
|
-
value: flattenText(
|
|
151
|
+
value: flattenText(message.content) || (images.length > 0 ? "(see attached image)" : "(no output)")
|
|
161
152
|
}
|
|
162
153
|
}]
|
|
163
154
|
});
|
|
164
155
|
pendingToolImages.push(...images);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const content = await userParts(message.content, resolveImage, signal);
|
|
159
|
+
if (content.length > 0) {
|
|
160
|
+
flushToolImages();
|
|
161
|
+
prompt.push({
|
|
162
|
+
role: "user",
|
|
163
|
+
content
|
|
164
|
+
});
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
flushToolImages();
|
package/dist/wire.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as serializeCallOptions, i as resolveReasoningWire, n as mapUsage, o as serializeCallOptionsWithImages, r as translate, t as mapFinishReason } from "./translate-
|
|
1
|
+
import { a as serializeCallOptions, i as resolveReasoningWire, n as mapUsage, o as serializeCallOptionsWithImages, r as translate, t as mapFinishReason } from "./translate-D9PyJrw2.mjs";
|
|
2
2
|
export { mapFinishReason, mapUsage, resolveReasoningWire, serializeCallOptions, serializeCallOptionsWithImages, translate };
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/dsh-llm-openai-compatible",
|
|
3
|
-
"version": "0.0.9-alpha.
|
|
3
|
+
"version": "0.0.9-alpha.7",
|
|
4
4
|
"description": "OpenAI-compatible LLM adapter plugin for DeepSeek Harness with configurable default sampling parameters (temperature / topP / topK / penalties / seed) over a providers dict.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"url": "https://github.com/morlay/dsh-plugin.git"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
+
"locale/*.json",
|
|
19
20
|
"dist",
|
|
20
21
|
"src",
|
|
21
22
|
"cordis.patch.yml",
|
|
@@ -27,24 +28,25 @@
|
|
|
27
28
|
"./invariant": "./dist/invariant.mjs",
|
|
28
29
|
"./wire": "./dist/wire.mjs",
|
|
29
30
|
"./package.json": "./package.json",
|
|
30
|
-
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
31
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
32
|
+
"./locale/*.json": "./locale/*.json"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
33
35
|
"@ai-sdk/openai-compatible": "^3.0.32",
|
|
34
36
|
"@ai-sdk/provider": "^4.0.7",
|
|
35
|
-
"@deepseek-ai/dsh-util-values": "0.1.
|
|
37
|
+
"@deepseek-ai/dsh-util-values": "0.1.7-alpha.2"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
|
38
|
-
"@deepseek-ai/cordis": "4.0.
|
|
39
|
-
"@deepseek-ai/dsh-anonymous-user-id": "0.1.
|
|
40
|
-
"@deepseek-ai/dsh-attachment": "0.1.
|
|
41
|
-
"@deepseek-ai/dsh-credentials": "0.1.
|
|
42
|
-
"@deepseek-ai/dsh-invariants": "0.1.
|
|
43
|
-
"@deepseek-ai/dsh-launch-environment": "0.1.
|
|
44
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
45
|
-
"@deepseek-ai/dsh-settings": "0.1.
|
|
46
|
-
"@deepseek-ai/dsh-timeout": "0.1.
|
|
47
|
-
"@deepseek-ai/schemastery": "3.18.
|
|
40
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
41
|
+
"@deepseek-ai/dsh-anonymous-user-id": "0.1.7-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-attachment": "0.1.7-alpha.2",
|
|
43
|
+
"@deepseek-ai/dsh-credentials": "0.1.7-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-invariants": "0.1.7-alpha.2",
|
|
45
|
+
"@deepseek-ai/dsh-launch-environment": "0.1.7-alpha.2",
|
|
46
|
+
"@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
|
|
47
|
+
"@deepseek-ai/dsh-settings": "0.1.7-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-timeout": "0.1.7-alpha.2",
|
|
49
|
+
"@deepseek-ai/schemastery": "3.18.4"
|
|
48
50
|
},
|
|
49
51
|
"dsh": {
|
|
50
52
|
"bundle": {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Context } from "@deepseek-ai/cordis";
|
|
1
|
+
import type { Context, Volatile } from "@deepseek-ai/cordis";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import {
|
|
4
4
|
LlmError,
|
|
@@ -82,9 +82,17 @@ export interface ProviderProfileSource {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export interface Config {
|
|
85
|
-
|
|
85
|
+
/**
|
|
86
|
+
* provider 路由,key 是路由键。**volatile**:值经 Loader 的引用读取
|
|
87
|
+
* (`config.providers.get()`),设置页改它时不需要重挂这行插件——上游 0.1.7 起
|
|
88
|
+
* 「运行期可改」只有这一条路(旧的 settings namespace section 覆盖已取消)。
|
|
89
|
+
*/
|
|
90
|
+
providers: Volatile<Record<string, ProviderProfileSource>>;
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
/** 解析成普通值之后的配置形状(校验与解析只认它)。 */
|
|
94
|
+
export type Options = { [K in keyof Config]?: Config[K] extends Volatile<infer T> ? T : never };
|
|
95
|
+
|
|
88
96
|
const modelSchema = z.object({
|
|
89
97
|
id: z.string().required(),
|
|
90
98
|
name: z.string(),
|
|
@@ -95,7 +103,7 @@ const modelSchema = z.object({
|
|
|
95
103
|
reasoningEfforts: z.union([z.const(false), z.dict(z.union([z.string(), z.const(null)]))]),
|
|
96
104
|
});
|
|
97
105
|
|
|
98
|
-
const providerSchema = z.object({
|
|
106
|
+
const providerSchema: z<ProviderProfileSource> = z.object({
|
|
99
107
|
apiKeyEnv: z.string().role("credential-ref"),
|
|
100
108
|
displayName: z.string(),
|
|
101
109
|
baseURL: z.string().required(),
|
|
@@ -120,8 +128,8 @@ const providerSchema = z.object({
|
|
|
120
128
|
retryPolicy: RetryPolicySchema,
|
|
121
129
|
});
|
|
122
130
|
|
|
123
|
-
export const Config
|
|
124
|
-
providers: z.dict(providerSchema).default({}),
|
|
131
|
+
export const Config = z.object({
|
|
132
|
+
providers: z.dict(providerSchema).default({}).volatile(),
|
|
125
133
|
});
|
|
126
134
|
|
|
127
135
|
function isReasoningEffort(value: string): value is ReasoningEffort {
|
|
@@ -355,7 +363,7 @@ export function resolveProfiles(
|
|
|
355
363
|
return resolved;
|
|
356
364
|
}
|
|
357
365
|
|
|
358
|
-
export function assertServiceable(config:
|
|
366
|
+
export function assertServiceable(config: Options): void {
|
|
359
367
|
resolveProfiles(config.providers);
|
|
360
368
|
}
|
|
361
369
|
|
|
@@ -369,10 +377,13 @@ function registrationFacts(profiles: ReadonlyMap<string, ResolvedProviderProfile
|
|
|
369
377
|
.sort((left, right) => left.provider.localeCompare(right.provider));
|
|
370
378
|
}
|
|
371
379
|
|
|
372
|
-
function directoryEntries(
|
|
380
|
+
function directoryEntries(
|
|
381
|
+
profiles: ReadonlyMap<string, ResolvedProviderProfile>,
|
|
382
|
+
settingsNs: string,
|
|
383
|
+
): {
|
|
373
384
|
provider: string;
|
|
374
385
|
displayName: string;
|
|
375
|
-
settingsNs:
|
|
386
|
+
settingsNs: string;
|
|
376
387
|
settingsPath: readonly string[];
|
|
377
388
|
declared: boolean;
|
|
378
389
|
}[] {
|
|
@@ -381,7 +392,7 @@ function directoryEntries(profiles: ReadonlyMap<string, ResolvedProviderProfile>
|
|
|
381
392
|
{
|
|
382
393
|
provider: string;
|
|
383
394
|
displayName: string;
|
|
384
|
-
settingsNs:
|
|
395
|
+
settingsNs: string;
|
|
385
396
|
settingsPath: readonly string[];
|
|
386
397
|
declared: boolean;
|
|
387
398
|
}
|
|
@@ -390,7 +401,7 @@ function directoryEntries(profiles: ReadonlyMap<string, ResolvedProviderProfile>
|
|
|
390
401
|
entries.set(provider, {
|
|
391
402
|
provider,
|
|
392
403
|
displayName: profile.displayName,
|
|
393
|
-
settingsNs
|
|
404
|
+
settingsNs,
|
|
394
405
|
settingsPath: ["providers", provider],
|
|
395
406
|
declared: true,
|
|
396
407
|
});
|
|
@@ -399,14 +410,18 @@ function directoryEntries(profiles: ReadonlyMap<string, ResolvedProviderProfile>
|
|
|
399
410
|
}
|
|
400
411
|
|
|
401
412
|
export function apply(ctx: Context, config: Config): void {
|
|
402
|
-
|
|
403
|
-
|
|
413
|
+
// `settingsNs` 是**行 id**(不是包短名):设置页按 entry 定位这份配置。
|
|
414
|
+
const settingsNs = ctx.fiber.entry?.options.id ?? NS;
|
|
415
|
+
// 引用读回的是 schema 解析后的形状(只读、可选),不是手写的 `Options`。
|
|
416
|
+
let lastRaw: ReturnType<Config["providers"]["get"]> | undefined;
|
|
404
417
|
let memoized: Map<string, ResolvedProviderProfile> | undefined;
|
|
405
418
|
|
|
406
419
|
const profiles = (): ReadonlyMap<string, ResolvedProviderProfile> => {
|
|
407
|
-
const raw =
|
|
420
|
+
const raw = config.providers.get();
|
|
408
421
|
if (raw === lastRaw && memoized !== void 0) return memoized;
|
|
409
|
-
|
|
422
|
+
// 引用读回的是 schema 解析后的只读形状(`exactOptionalPropertyTypes` 下与手写的可选字段形态不同),
|
|
423
|
+
// 这里按上游 llm-pi-ai 的做法转换一次再解析。
|
|
424
|
+
const next = resolveProfiles(raw as Record<string, ProviderProfileSource> | undefined);
|
|
410
425
|
lastRaw = raw;
|
|
411
426
|
memoized = next;
|
|
412
427
|
return next;
|
|
@@ -443,7 +458,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
443
458
|
let directory: ReturnType<typeof ctx.llm.registerConfigurableProviders> | undefined;
|
|
444
459
|
let directoryFacts: unknown[] | undefined;
|
|
445
460
|
const ensureDirectory = () => {
|
|
446
|
-
const entries = directoryEntries(profiles());
|
|
461
|
+
const entries = directoryEntries(profiles(), settingsNs);
|
|
447
462
|
if (deepEqualJson(entries, directoryFacts)) return;
|
|
448
463
|
if (directory === void 0) directory = ctx.llm.registerConfigurableProviders(entries);
|
|
449
464
|
else directory.replace(entries);
|
|
@@ -468,30 +483,35 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
468
483
|
registeredFacts = facts;
|
|
469
484
|
};
|
|
470
485
|
ensureRegistrationFacts();
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
486
|
+
const refresh = (): void => {
|
|
487
|
+
try {
|
|
488
|
+
ensureRegistrationFacts();
|
|
489
|
+
} catch (error) {
|
|
490
|
+
ctx.logger.error(
|
|
491
|
+
"llm-openai-compatible: keeping the previously registered routes after a refused update",
|
|
492
|
+
);
|
|
493
|
+
ctx.logger.error(error);
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
ensureDirectory();
|
|
497
|
+
} catch (error) {
|
|
498
|
+
ctx.logger.error(
|
|
499
|
+
"llm-openai-compatible: keeping the previous configurable-provider directory after a refused update",
|
|
500
|
+
);
|
|
501
|
+
ctx.logger.error(error);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
// volatile 更新(设置页保存)只把新值提交进运行引用并广播,不重挂这一行:这里重算路由注册与
|
|
505
|
+
// 可配置 provider 目录(上游 0.1.7 的运行期改配置机制;`profiles()` 读的就是引用里的新值)。
|
|
506
|
+
ctx.on("loader/volatile-update", refresh);
|
|
507
|
+
// 候选配置在提交前先校验:无效更新被拒绝,运行引用保持原值(loader 只记录这次拒绝)。
|
|
508
|
+
ctx.on("internal/config", function (this: Context["fiber"], _raw, next) {
|
|
509
|
+
const raw: unknown = next();
|
|
510
|
+
if (this !== ctx.fiber) return raw;
|
|
511
|
+
const candidate = Config(raw as Options);
|
|
512
|
+
assertServiceable({
|
|
513
|
+
providers: structuredClone(candidate.providers.get()) as Record<string, ProviderProfileSource>,
|
|
495
514
|
});
|
|
515
|
+
return raw;
|
|
496
516
|
});
|
|
497
517
|
}
|
package/src/serialize.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
projectOffloadedImages,
|
|
7
7
|
requiredImageOffload,
|
|
8
8
|
} from "@deepseek-ai/dsh-llm";
|
|
9
|
-
import type { ContentBlock, GenerateOptions,
|
|
9
|
+
import type { ContentBlock, GenerateOptions, RequestMessage } from "@deepseek-ai/dsh-llm";
|
|
10
10
|
import { AttachmentError } from "@deepseek-ai/dsh-attachment";
|
|
11
11
|
import type { AttachmentStore } from "@deepseek-ai/dsh-attachment";
|
|
12
12
|
import type {
|
|
@@ -83,9 +83,14 @@ function assertTextOnly(blocks: readonly ContentBlock[]): void {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function assertSupportedImageRoles(messages: readonly
|
|
86
|
+
function assertSupportedImageRoles(messages: readonly RequestMessage[]): void {
|
|
87
87
|
for (const message of messages) {
|
|
88
|
-
|
|
88
|
+
// 图片只出现在 user 与 tool 消息里;tool 结果的图片由 serializePrompt 聚成一条 user 消息发出。
|
|
89
|
+
if (
|
|
90
|
+
message.role !== "user" &&
|
|
91
|
+
message.role !== "tool" &&
|
|
92
|
+
contentHasImage(message.content)
|
|
93
|
+
) {
|
|
89
94
|
throw new LlmError(
|
|
90
95
|
`The OpenAI-compatible chat-completions adapter cannot represent image content in a ${message.role} message.`,
|
|
91
96
|
"UNSUPPORTED_CONTENT",
|
|
@@ -94,7 +99,10 @@ function assertSupportedImageRoles(messages: readonly Message[]): void {
|
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
function assertRetainedImagesFit(
|
|
102
|
+
function assertRetainedImagesFit(
|
|
103
|
+
messages: readonly RequestMessage[],
|
|
104
|
+
maxRequestImageBytes: number,
|
|
105
|
+
): void {
|
|
98
106
|
const offloadImages = requiredImageOffload(
|
|
99
107
|
messages,
|
|
100
108
|
{ representation: "base64", maxBytes: maxRequestImageBytes },
|
|
@@ -133,7 +141,7 @@ async function imagePart(
|
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
function assistantParts(
|
|
136
|
-
message:
|
|
144
|
+
message: Extract<RequestMessage, { role: "assistant" }>,
|
|
137
145
|
toolNames: Map<string, string>,
|
|
138
146
|
): Extract<LanguageModelV4Prompt[number], { role: "assistant" }>["content"] {
|
|
139
147
|
const parts: Extract<LanguageModelV4Prompt[number], { role: "assistant" }>["content"] = [];
|
|
@@ -190,9 +198,6 @@ async function userParts(
|
|
|
190
198
|
);
|
|
191
199
|
parts.push(await resolveImage(block, signal));
|
|
192
200
|
break;
|
|
193
|
-
case "tool-result":
|
|
194
|
-
parts.push(...(await userParts(block.content, resolveImage, signal)));
|
|
195
|
-
break;
|
|
196
201
|
default:
|
|
197
202
|
break;
|
|
198
203
|
}
|
|
@@ -201,7 +206,7 @@ async function userParts(
|
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
async function serializePrompt(
|
|
204
|
-
messages: readonly
|
|
209
|
+
messages: readonly RequestMessage[],
|
|
205
210
|
resolveImage:
|
|
206
211
|
| ((
|
|
207
212
|
block: Extract<ContentBlock, { type: "image" }>,
|
|
@@ -227,6 +232,20 @@ async function serializePrompt(
|
|
|
227
232
|
pendingToolImages = [];
|
|
228
233
|
};
|
|
229
234
|
for (const message of messages) {
|
|
235
|
+
// V4 把 developer 消息与 tool-change 块持久化下来,但 provider 侧序列化明确不支持它们
|
|
236
|
+
// (与上游 `llm-deepseek` 同口径:先失败,等生产者与消费方一起实现)。
|
|
237
|
+
if (message.role === "developer") {
|
|
238
|
+
throw new LlmError(
|
|
239
|
+
"The OpenAI-compatible chat-completions adapter cannot serialize developer messages.",
|
|
240
|
+
"UNSUPPORTED_CONTENT",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (message.content.some((block) => block.type === "tool-addition" || block.type === "tool-removal")) {
|
|
244
|
+
throw new LlmError(
|
|
245
|
+
"The OpenAI-compatible chat-completions adapter cannot serialize tool-change blocks outside developer messages.",
|
|
246
|
+
"UNSUPPORTED_CONTENT",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
230
249
|
if (message.role === "system") {
|
|
231
250
|
flushToolImages();
|
|
232
251
|
prompt.push({ role: "system", content: flattenText(message.content) });
|
|
@@ -238,37 +257,38 @@ async function serializePrompt(
|
|
|
238
257
|
if (parts.length > 0) prompt.push({ role: "assistant", content: parts });
|
|
239
258
|
continue;
|
|
240
259
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const content = await userParts(regular, resolveImage, signal);
|
|
244
|
-
if (content.length > 0 || toolResults.length === 0) {
|
|
245
|
-
flushToolImages();
|
|
246
|
-
prompt.push({ role: "user", content });
|
|
247
|
-
}
|
|
248
|
-
for (const result of toolResults) {
|
|
260
|
+
if (message.role === "tool") {
|
|
261
|
+
// 结果消息自己带 `toolCallId` 与结果块(V4 起结果不再是 user 消息里的一个块)。
|
|
249
262
|
const images: UserContentPart[] = [];
|
|
250
263
|
if (resolveImage !== void 0) {
|
|
251
|
-
for (const block of
|
|
264
|
+
for (const block of message.content) {
|
|
252
265
|
if (block.type === "image") images.push(await resolveImage(block, signal));
|
|
253
266
|
}
|
|
254
267
|
}
|
|
268
|
+
flushToolImages();
|
|
255
269
|
prompt.push({
|
|
256
270
|
role: "tool",
|
|
257
271
|
content: [
|
|
258
272
|
{
|
|
259
273
|
type: "tool-result",
|
|
260
|
-
toolCallId:
|
|
261
|
-
toolName: toolNames.get(
|
|
274
|
+
toolCallId: message.toolCallId,
|
|
275
|
+
toolName: toolNames.get(message.toolCallId) ?? "",
|
|
262
276
|
output: {
|
|
263
277
|
type: "text",
|
|
264
278
|
value:
|
|
265
|
-
flattenText(
|
|
279
|
+
flattenText(message.content) ||
|
|
266
280
|
(images.length > 0 ? "(see attached image)" : "(no output)"),
|
|
267
281
|
},
|
|
268
282
|
},
|
|
269
283
|
],
|
|
270
284
|
});
|
|
271
285
|
pendingToolImages.push(...images);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const content = await userParts(message.content, resolveImage, signal);
|
|
289
|
+
if (content.length > 0) {
|
|
290
|
+
flushToolImages();
|
|
291
|
+
prompt.push({ role: "user", content });
|
|
272
292
|
}
|
|
273
293
|
}
|
|
274
294
|
flushToolImages();
|