@siliconflow-official/dsh-llm-siliconflow 0.1.0-rc.7 → 0.2.0-rc.2
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 +15 -4
- package/lib/bin.js +93 -10
- package/lib/index.js +2 -2
- package/lib/types/adapter.d.ts +20 -1
- package/lib/types/index.d.ts +18 -2
- package/lib/types/serialize.d.ts +20 -5
- package/lib/types/setup.d.ts +12 -5
- package/lib/types/types.d.ts +14 -2
- package/lib/{types-CmhCs_oH.js → types-iMqgaeX4.js} +246 -45
- package/package.json +14 -10
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @siliconflow-official/dsh-llm-siliconflow
|
|
2
2
|
|
|
3
|
-
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) LLM 接缝的 SiliconFlow chat-completions 适配器插件:用直接 `fetch` + SSE(由 `eventsource-parser` 分帧)把 SiliconFlow 的 OpenAI 兼容线上格式翻译成 `StreamChunk` 协议。SiliconFlow 托管着广泛的开源模型目录,其中包含 delta 携带 `reasoning_content` 的推理模型(DeepSeek-R1、QwQ、Kimi-K2-Thinking)——适配器把该通道翻译成 harness reasoning
|
|
3
|
+
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) LLM 接缝的 SiliconFlow chat-completions 适配器插件:用直接 `fetch` + SSE(由 `eventsource-parser` 分帧)把 SiliconFlow 的 OpenAI 兼容线上格式翻译成 `StreamChunk` 协议。SiliconFlow 托管着广泛的开源模型目录,其中包含 delta 携带 `reasoning_content` 的推理模型(DeepSeek-R1、QwQ、Kimi-K2-Thinking)——适配器把该通道翻译成 harness reasoning 块,并在工具调用轮次按这些模型的要求将其回传。同时支持视觉语言模型(VLM):当 harness 挂载了附件存储服务(`ctx.attachments`)时,用户消息中的图片块会被解析为 base64 data URL 并序列化为 OpenAI 兼容的 `image_url` 多模态内容部分。
|
|
4
4
|
|
|
5
5
|
本包拥有 `siliconflow` 提供方路由,因此部署只需提供一个 SiliconFlow API key 即可使用。其模型选择器从实时 `GET /models?sub_type=chat` 列表按端点顺序填充;配置的 `models` 列表是在没有 key 或发现失败时展示的回退目录。这是一个纯 OpenAI 兼容端点,没有 `thinking`/`reasoning_effort` 开关,因此适配器不暴露任何推理档位元数据、也不序列化任何推理档位字段:推理模型通过其目录 id 选择,请求上显式指定 `reasoningEffort` 会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 被拒绝。为 `siliconflow` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
|
6
6
|
|
|
@@ -34,6 +34,14 @@ dsh-siliconflow-setup
|
|
|
34
34
|
export SILICONFLOW_API_KEY=sk-... # 或写入 $DSH_HOME/.credentials.yaml
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
写入 `.credentials.yaml` 时使用 dsh ≥ 0.1.2 的 version-1 布局(`version: 1` + `refs:` 嵌套);向导会识别并就地升级本向导早期版本写入的 pre-release 顶层扁平布局,其他无法识别的文档会明确报错而不是被改写:
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
version: 1
|
|
41
|
+
refs:
|
|
42
|
+
SILICONFLOW_API_KEY: sk-...
|
|
43
|
+
```
|
|
44
|
+
|
|
37
45
|
在包发布到 npm 之前,先用 `pnpm install && pnpm build` 构建出 `lib/`,再从本地路径安装:
|
|
38
46
|
|
|
39
47
|
```sh
|
|
@@ -90,7 +98,9 @@ nohup npx @deepseek-ai/dsh --profile headless "任务" > ~/.dsh/task.log 2>&1 &
|
|
|
90
98
|
- id: deepseek-ai/DeepSeek-V4-Flash
|
|
91
99
|
```
|
|
92
100
|
|
|
93
|
-
|
|
101
|
+
目录条目可携带 `inputModalities` 字段(`['text']` 或 `['text', 'image']`)以显式声明模型接受的输入模态;省略时适配器从内置的权威 VLM 模型集合(从模型广场 `vlm` 属性提取,静态维护,最后更新 2026-08-22)查找。
|
|
102
|
+
|
|
103
|
+
插件把单个提供方路由 `siliconflow` 连同其已解析的 `retryPolicy` 一并注册。请求用 `provider: siliconflow` 选中它;其 `model` 原样作为线上 `model` 字符串透传,因此更换 SiliconFlow 模型不需要生命周期级重新注册。线上模型 id 是 SiliconFlow 的 `org/model` 写法(如 `deepseek-ai/DeepSeek-V4-Flash`),绝不是短别名。省略 `models` 时保留一份由当前托管对话模型(含 VLM)组成的回退目录;显式列表会替换这些默认值,而 `models: []` 则一个都不通告。目录条目通过 `ctx.llm.listModels('siliconflow')` 暴露给 ACP 编辑器与 Web 选择器这类客户端,但始终是建议性的:未列出的模型 id 依然原样透传。省略的条目名默认等于其 id。
|
|
94
104
|
|
|
95
105
|
`contextWindow` 按模型可选。`ctx.llm.resolveModelInfo('siliconflow', model).context` 先返回精确值——来自配置条目或温热的发现缓存——再对未被任何来源定容的模型回退到 `defaultContextWindow`。适配器默认值是 32,768;SiliconFlow 目录大致横跨 8k 到 1M 上下文(GLM-5.2、DeepSeek-V4-Pro/Flash 均支持 1M),因此披露了上下文的发现列表是权威值,回退值仅在没有任何来源披露时使用。对压力敏感的插件由此获得部署自有的容量,而不把模型选择器当作权威。
|
|
96
106
|
|
|
@@ -161,10 +171,11 @@ nohup npx @deepseek-ai/dsh --profile headless "任务" > ~/.dsh/task.log 2>&1 &
|
|
|
161
171
|
|
|
162
172
|
## Known Limitations and Deferred Work
|
|
163
173
|
|
|
164
|
-
- **回退 `models` 列表是手工维护的** ——
|
|
174
|
+
- **回退 `models` 列表是手工维护的** —— 默认值是一份快照,仅在发现无法运行时展示;实时列表才是权威目录。
|
|
165
175
|
- **发现不跨 baseURL 变化缓存** —— 缓存按端点键控,因此改指路由会在下一次 `listModels` 重新询问。
|
|
166
176
|
- **settings 的 `models` 列表整体替换组合列表** —— settings 层合并在字段粒度进行,数组是一个字段;按条目合并目录需要键控结构。
|
|
167
177
|
- **未映射 `tool_choice`** —— 不属于核心词汇表(MVP 裁剪,与 pi-ai 和 DeepSeek 双胞胎相同)。
|
|
168
178
|
- **请求使用原始 `fetch`,而非 `@cordisjs/plugin-http`** —— 没有共享代理/拦截配置;待有第二个直接 fetch 适配器需要时再采用(`TODO(http)`)。
|
|
169
179
|
- **序列化把 user 与 tool-result 内容扁平化为文本块** —— 插件添加的块类型被跳过,空工具输出以字面 `(no output)` 上线。
|
|
170
|
-
-
|
|
180
|
+
- **VLM 模型集与回退目录的 contextWindow 均为静态数据** —— SiliconFlow 的 OpenAI 兼容 `GET /models` API 不返回模型的 `vlm` 属性或 `contextLen` 字段,因此适配器内置了从模型广场(siliconflow.cn/models)SSR 数据提取的权威 VLM 模型 ID 集合(当前 23 个)和回退目录的上下文窗口配置。这些静态数据会随模型广场更新而定期同步,直至 OpenAPI 本身暴露相关属性(如 `sub_type=vision` 或模型列表中的 `vlm` 字段)后被运行时查询取代。目录条目可通过 `inputModalities` 字段显式声明以覆盖静态集合。
|
|
181
|
+
- **图片附件依赖存储服务** —— 用户消息中的图片块通过 `ctx.attachments` 解析为 base64 data URL;未挂载该服务时图片被替换为 `[image omitted]` 占位文本,请求仍可继续。工具结果中的图片始终被替换为占位文本。
|
package/lib/bin.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as PUBLIC_BASE_URL,
|
|
2
|
+
import { a as PUBLIC_BASE_URL, g as discoverChatModels, i as PROVIDER, n as DEFAULT_API_KEY_ENV, r as DEFAULT_MODELS } from "./types-iMqgaeX4.js";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { stdin, stdout } from "node:process";
|
|
5
5
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { dirname, join, resolve } from "node:path";
|
|
8
|
-
import { parseDocument } from "yaml";
|
|
8
|
+
import { Pair, YAMLMap, parseDocument } from "yaml";
|
|
9
9
|
//#region lib/types/setup.js
|
|
10
10
|
/**
|
|
11
11
|
* Interactive setup wizard core for {@link @siliconflow-official/dsh-llm-siliconflow}:
|
|
@@ -45,11 +45,81 @@ function settingsPath(home) {
|
|
|
45
45
|
function isEnoent(error) {
|
|
46
46
|
return error.code === "ENOENT";
|
|
47
47
|
}
|
|
48
|
+
/** The credentials document layout version this wizard reads and writes. */
|
|
49
|
+
const CREDENTIALS_LAYOUT_VERSION = 1;
|
|
50
|
+
/** POSIX identifier rule the credentials seam addresses references by. */
|
|
51
|
+
const REF_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
48
52
|
/**
|
|
49
|
-
*
|
|
53
|
+
* Recognize one parsed credentials document.
|
|
54
|
+
* @param document - the parsed document; parse errors make it unrecognizable.
|
|
55
|
+
* @returns the admitted layout with its references — plus, under `extraKeys`,
|
|
56
|
+
* any top-level key a pre-fix release of this wizard left beside `version`,
|
|
57
|
+
* held out of `entries` so reads treat them as absent while the write path
|
|
58
|
+
* folds them back under `refs` — or `undefined` when the document does not
|
|
59
|
+
* parse, holds an unknown `version`, carries a `refs` section that is not a
|
|
60
|
+
* mapping of reference names to non-empty strings, or holds any other
|
|
61
|
+
* top-level entry this build cannot attribute to its own predecessor.
|
|
62
|
+
*/
|
|
63
|
+
function admitCredentialsDocument(document) {
|
|
64
|
+
if (document.errors.length > 0) return void 0;
|
|
65
|
+
const root = document.toJS() ?? {};
|
|
66
|
+
if (typeof root !== "object" || root === null) return void 0;
|
|
67
|
+
const fields = root;
|
|
68
|
+
const entries = /* @__PURE__ */ new Map();
|
|
69
|
+
if (!("version" in fields)) {
|
|
70
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
71
|
+
if (!REF_NAME_PATTERN.test(key)) return void 0;
|
|
72
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
73
|
+
entries.set(key, value);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
kind: "flat",
|
|
77
|
+
entries
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (fields.version !== CREDENTIALS_LAYOUT_VERSION) return void 0;
|
|
81
|
+
const refs = fields["refs"];
|
|
82
|
+
if (refs !== void 0) {
|
|
83
|
+
if (typeof refs !== "object" || refs === null) return void 0;
|
|
84
|
+
for (const [key, value] of Object.entries(refs)) {
|
|
85
|
+
if (!REF_NAME_PATTERN.test(key)) return void 0;
|
|
86
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
87
|
+
entries.set(key, value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const extraKeys = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
92
|
+
if (key === "version" || key === "refs" || key === "records") continue;
|
|
93
|
+
if (!REF_NAME_PATTERN.test(key) || typeof value !== "string" || value.length === 0) return void 0;
|
|
94
|
+
extraKeys.set(key, value);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
kind: "versioned",
|
|
98
|
+
entries,
|
|
99
|
+
...extraKeys.size > 0 ? { extraKeys } : {}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Nest a pre-release flat document under `refs:` with a `version` stamp. The
|
|
104
|
+
* whole current root — every flat entry, comments included — becomes the
|
|
105
|
+
* `refs` section verbatim, so every other provider's key migrates in the same
|
|
106
|
+
* write; the new root carries `version` and `refs` in that order, matching how
|
|
107
|
+
* `dsh-credentials-local` writes one from scratch.
|
|
108
|
+
* @param document - the admitted flat document to upgrade in place.
|
|
109
|
+
*/
|
|
110
|
+
function upgradeFlatDocument(document) {
|
|
111
|
+
const refs = document.contents instanceof YAMLMap ? document.contents : new YAMLMap();
|
|
112
|
+
const root = new YAMLMap();
|
|
113
|
+
root.add(new Pair("version", CREDENTIALS_LAYOUT_VERSION));
|
|
114
|
+
root.add(new Pair("refs", refs));
|
|
115
|
+
document.contents = root;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Read one credential reference from a versioned or pre-release flat document.
|
|
50
119
|
* @param path - the credentials document path.
|
|
51
|
-
* @param keyEnv - the
|
|
52
|
-
* @returns the stored value, or `undefined` when
|
|
120
|
+
* @param keyEnv - the reference name to read (e.g. `SILICONFLOW_API_KEY`).
|
|
121
|
+
* @returns the stored value, or `undefined` when the reference is absent, the
|
|
122
|
+
* file is missing, or the document is not a recognizable layout.
|
|
53
123
|
*/
|
|
54
124
|
async function readCredential(path, keyEnv) {
|
|
55
125
|
let text;
|
|
@@ -59,18 +129,31 @@ async function readCredential(path, keyEnv) {
|
|
|
59
129
|
if (isEnoent(error)) return void 0;
|
|
60
130
|
throw error;
|
|
61
131
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
132
|
+
const admitted = admitCredentialsDocument(parseDocument(text));
|
|
133
|
+
if (admitted === void 0) return void 0;
|
|
134
|
+
return admitted.entries.get(keyEnv);
|
|
64
135
|
}
|
|
65
136
|
/**
|
|
66
|
-
*
|
|
137
|
+
* Write one credential reference into the versioned layout, preserving every
|
|
138
|
+
* other entry and comment. A pre-release flat document is upgraded in place,
|
|
139
|
+
* and a top-level key a pre-fix release of this wizard left beside `version`
|
|
140
|
+
* is folded back under `refs`, so one run repairs the file its predecessor
|
|
141
|
+
* corrupted. An unrecognized document fails loud instead of being rewritten —
|
|
142
|
+
* a silent rewrite would hide why the running harness rejects it.
|
|
67
143
|
* @param path - the credentials document path; created when absent.
|
|
68
|
-
* @param keyEnv - the
|
|
144
|
+
* @param keyEnv - the reference name to write.
|
|
69
145
|
* @param key - the value.
|
|
146
|
+
* @throws when the document exists but is not a recognizable credentials layout.
|
|
70
147
|
*/
|
|
71
148
|
async function writeCredential(path, keyEnv, key) {
|
|
72
149
|
const doc = await loadDocument(path);
|
|
73
|
-
doc
|
|
150
|
+
const admitted = admitCredentialsDocument(doc);
|
|
151
|
+
if (admitted === void 0) throw new Error(`setup: ${path} is not a recognizable credentials document (expected version 1 with a refs section, or the pre-release flat layout); fix it before running setup`);
|
|
152
|
+
if (admitted.kind === "flat") upgradeFlatDocument(doc);
|
|
153
|
+
for (const extra of admitted.extraKeys?.keys() ?? []) doc.deleteIn([extra]);
|
|
154
|
+
for (const [extra, value] of admitted.extraKeys ?? /* @__PURE__ */ new Map()) doc.setIn(["refs", extra], value);
|
|
155
|
+
doc.setIn(["version"], CREDENTIALS_LAYOUT_VERSION);
|
|
156
|
+
doc.setIn(["refs", keyEnv], key);
|
|
74
157
|
await persistDocument(path, doc);
|
|
75
158
|
}
|
|
76
159
|
/**
|
package/lib/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
export { Config, DEFAULT_API_KEY_ENV, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_MODELS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DISCOVERY_TTL_MS, PROVIDER, PUBLIC_BASE_URL, SiliconFlowAdapter, apply, discoverChatModels, inject, listingUrl, name, readListing, resolveAdapterOptions };
|
|
1
|
+
import { _ as listingUrl, a as PUBLIC_BASE_URL, c as name, d as DEFAULT_MAX_TOKENS, f as DEFAULT_STREAM_IDLE_TIMEOUT_MS, g as discoverChatModels, h as inferInputModalities, i as PROVIDER, l as resolveAdapterOptions, m as SiliconFlowAdapter, n as DEFAULT_API_KEY_ENV, o as apply, p as DISCOVERY_TTL_MS, r as DEFAULT_MODELS, s as inject, t as Config, u as DEFAULT_CONTEXT_WINDOW, v as readListing } from "./types-iMqgaeX4.js";
|
|
2
|
+
export { Config, DEFAULT_API_KEY_ENV, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_MODELS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DISCOVERY_TTL_MS, PROVIDER, PUBLIC_BASE_URL, SiliconFlowAdapter, apply, discoverChatModels, inferInputModalities, inject, listingUrl, name, readListing, resolveAdapterOptions };
|
package/lib/types/adapter.d.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* @module dsh-llm-siliconflow/adapter
|
|
9
9
|
*/
|
|
10
10
|
import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
11
|
-
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
11
|
+
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ModelModality, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
12
12
|
import type { CredentialRef } from '@deepseek-ai/dsh-credentials';
|
|
13
|
+
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
13
14
|
import type { AnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id';
|
|
14
15
|
import type { WireError } from './types.ts';
|
|
15
16
|
/** One optional model entry advertised by the direct-fetch adapter. */
|
|
@@ -24,6 +25,8 @@ export interface SiliconFlowCatalogModel {
|
|
|
24
25
|
contextWindow?: number;
|
|
25
26
|
/** Per-request output cap for this model; omission falls back to the profile's {@link SiliconFlowConnectionOptions.maxTokens}. */
|
|
26
27
|
maxTokens?: number;
|
|
28
|
+
/** Accepted input modalities; omitted infers from the built-in VLM model set. */
|
|
29
|
+
inputModalities?: ModelModality[];
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
29
32
|
* Validated connection facts for one operation. The plugin's
|
|
@@ -65,6 +68,12 @@ export interface SiliconFlowAdapterOptions {
|
|
|
65
68
|
resolveApiKey: (connection: SiliconFlowConnectionOptions) => Promise<string>;
|
|
66
69
|
/** Resolve the harness-home anonymous id shared with telemetry and feedback. */
|
|
67
70
|
resolveUserId: () => AnonymousUserId;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve one image attachment to a base64 data URL for wire serialization.
|
|
73
|
+
* Returns `undefined` when the attachment store is unavailable or the read
|
|
74
|
+
* fails; the serializer substitutes the `OFFLOADED_IMAGE_TEXT` sentinel.
|
|
75
|
+
*/
|
|
76
|
+
resolveImage?: (ref: ImageAttachmentRef) => Promise<string | undefined>;
|
|
68
77
|
}
|
|
69
78
|
/** Default maximum idle interval while an adapter stream read is outstanding. */
|
|
70
79
|
export declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000;
|
|
@@ -74,6 +83,16 @@ export declare const DEFAULT_CONTEXT_WINDOW = 32768;
|
|
|
74
83
|
export declare const DEFAULT_MAX_TOKENS = 8192;
|
|
75
84
|
/** How long a cached model-listing discovery stays fresh before the next `listModels` re-interrogates. */
|
|
76
85
|
export declare const DISCOVERY_TTL_MS: number;
|
|
86
|
+
/**
|
|
87
|
+
* Determine the input modalities for a SiliconFlow model id.
|
|
88
|
+
*
|
|
89
|
+
* Uses the authoritative VLM model set from the SiliconFlow marketplace rather
|
|
90
|
+
* than naming-pattern heuristics. Catalog entries can declare explicit
|
|
91
|
+
* `inputModalities` to override this lookup.
|
|
92
|
+
* @param id - the wire model id (e.g. `Qwen/Qwen3-VL-8B-Instruct`).
|
|
93
|
+
* @returns `['text', 'image']` when the id is a known VLM, `['text']` otherwise.
|
|
94
|
+
*/
|
|
95
|
+
export declare function inferInputModalities(id: string): readonly ModelModality[];
|
|
77
96
|
/**
|
|
78
97
|
* Map an HTTP status to a stable LlmError code.
|
|
79
98
|
* @param status - status of a non-2xx provider response.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* restarting anything, while an in-flight stream keeps the facts it started
|
|
9
9
|
* with. The one registration-captured fact — the retry policy — re-registers
|
|
10
10
|
* the route in place when it changes.
|
|
11
|
+
*
|
|
12
|
+
* When the optional `ctx.attachments` service is mounted, image blocks in user
|
|
13
|
+
* messages are resolved to base64 data URLs and serialized as OpenAI-compatible
|
|
14
|
+
* `image_url` content parts, enabling VLM models (Qwen3-VL, GLM-4.5V, etc.).
|
|
11
15
|
* @module @siliconflow-official/dsh-llm-siliconflow
|
|
12
16
|
*/
|
|
13
17
|
import type { Context } from '@deepseek-ai/cordis';
|
|
@@ -16,6 +20,7 @@ import type { RetryPolicyConfig } from '@deepseek-ai/dsh-llm';
|
|
|
16
20
|
import { type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment';
|
|
17
21
|
import type { SiliconFlowCatalogModel, SiliconFlowConnectionOptions } from './adapter.ts';
|
|
18
22
|
export { DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DISCOVERY_TTL_MS, SiliconFlowAdapter, } from './adapter.ts';
|
|
23
|
+
export { inferInputModalities } from './adapter.ts';
|
|
19
24
|
export { discoverChatModels, listingUrl, readListing } from './discovery.ts';
|
|
20
25
|
export type { SiliconFlowListingEntry } from './discovery.ts';
|
|
21
26
|
export type { SiliconFlowAdapterOptions, SiliconFlowCatalogModel, SiliconFlowConnectionOptions } from './adapter.ts';
|
|
@@ -26,7 +31,18 @@ export declare const inject: string[];
|
|
|
26
31
|
export declare const DEFAULT_API_KEY_ENV = "SILICONFLOW_API_KEY";
|
|
27
32
|
/** The single provider route this plugin owns. */
|
|
28
33
|
export declare const PROVIDER = "siliconflow";
|
|
29
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Fallback advisory catalog: widely hosted chat models including VLMs, also the
|
|
36
|
+
* setup CLI's discovery fallback.
|
|
37
|
+
*
|
|
38
|
+
* **Static data** — the `contextWindow` values and VLM flags are extracted from
|
|
39
|
+
* the SiliconFlow model marketplace (siliconflow.cn/models). The live
|
|
40
|
+
* `GET /models?sub_type=chat` API does not return context window or VLM metadata,
|
|
41
|
+
* so these fields are maintained by hand. This catalog will be kept in sync with
|
|
42
|
+
* the marketplace until the API exposes these attributes natively.
|
|
43
|
+
*
|
|
44
|
+
* Last updated: 2026-08-22. VLM entries declare `inputModalities` explicitly.
|
|
45
|
+
*/
|
|
30
46
|
export declare const DEFAULT_MODELS: SiliconFlowCatalogModel[];
|
|
31
47
|
/**
|
|
32
48
|
* Plugin config, validated by the same-named schemastery schema and doubling
|
|
@@ -44,7 +60,7 @@ export interface Config {
|
|
|
44
60
|
maxTokens?: number;
|
|
45
61
|
/** Positive context capacity used when the selected model has no exact value (default 32,768). */
|
|
46
62
|
defaultContextWindow?: number;
|
|
47
|
-
/** Advisory models shown by discovery consumers; defaults to
|
|
63
|
+
/** Advisory models shown by discovery consumers; defaults to widely hosted models including VLMs. */
|
|
48
64
|
models?: SiliconFlowCatalogModel[];
|
|
49
65
|
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
|
|
50
66
|
streamIdleTimeoutMs?: number;
|
package/lib/types/serialize.d.ts
CHANGED
|
@@ -3,28 +3,43 @@
|
|
|
3
3
|
* joined; assistant text becomes `content`, tool calls become `tool_calls`,
|
|
4
4
|
* and tool results become separate tool messages. Assistant reasoning is
|
|
5
5
|
* replayed as `reasoning_content` only on tool-call turns, as hosted reasoning
|
|
6
|
-
* models (DeepSeek-R1 and siblings) require.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* models (DeepSeek-R1 and siblings) require.
|
|
7
|
+
*
|
|
8
|
+
* **Multimodal**: image blocks in user messages are serialized as
|
|
9
|
+
* OpenAI-compatible `image_url` content parts with base64 data URLs. The
|
|
10
|
+
* serializer resolves image attachments through the optional attachment-store
|
|
11
|
+
* resolver; when no resolver is available, images are replaced with the
|
|
12
|
+
* `OFFLOADED_IMAGE_TEXT` sentinel. Tool-result content is flattened to text
|
|
13
|
+
* (images within tool results are also replaced with the sentinel). Unknown
|
|
14
|
+
* declaration-merged block types retain the adapter's documented extension
|
|
15
|
+
* fallback.
|
|
16
|
+
*
|
|
9
17
|
* @module dsh-llm-siliconflow/serialize
|
|
10
18
|
*/
|
|
11
19
|
import type { GenerateOptions, Message } from '@deepseek-ai/dsh-llm';
|
|
20
|
+
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
12
21
|
import type { WireMessage, WireRequest } from './types.ts';
|
|
13
22
|
/**
|
|
14
23
|
* Serialize the conversation. `tool-result` blocks become standalone
|
|
15
24
|
* `{role: 'tool'}` messages; the harness puts each tool result in its own
|
|
16
25
|
* user-role message, so a mixed user message contributes its text first and
|
|
17
26
|
* its tool results as separate wire messages after.
|
|
27
|
+
*
|
|
28
|
+
* Image blocks within user messages are resolved through the `resolveImage`
|
|
29
|
+
* callback; images within tool results are flattened to the sentinel text
|
|
30
|
+
* (tool results carry structured data, not multimodal content).
|
|
18
31
|
* @param messages - the harness conversation, in order.
|
|
32
|
+
* @param resolveImage - resolves an image attachment ref to a base64 data URL; returns `undefined` when unavailable.
|
|
19
33
|
* @returns the wire messages; order preserved, each tool result expanded into its own entry.
|
|
20
34
|
*/
|
|
21
|
-
export declare function serializeMessages(messages: Message[]): WireMessage[]
|
|
35
|
+
export declare function serializeMessages(messages: Message[], resolveImage?: (ref: ImageAttachmentRef) => Promise<string | undefined>): Promise<WireMessage[]>;
|
|
22
36
|
/**
|
|
23
37
|
* Build the full wire request. Always streaming (`stream: true`, usage
|
|
24
38
|
* reporting on); optional fields are omitted rather than sent as null, so
|
|
25
39
|
* provider defaults apply.
|
|
26
40
|
* @param options - the harness request (model, history, system, tools, sampling).
|
|
41
|
+
* @param resolveImage - resolves an image attachment ref to a base64 data URL; returns `undefined` when unavailable.
|
|
27
42
|
* @returns the chat-completions request body.
|
|
28
43
|
*/
|
|
29
|
-
export declare function serializeRequest(options: GenerateOptions): WireRequest
|
|
44
|
+
export declare function serializeRequest(options: GenerateOptions, resolveImage?: (ref: ImageAttachmentRef) => Promise<string | undefined>): Promise<WireRequest>;
|
|
30
45
|
//# sourceMappingURL=serialize.d.ts.map
|
package/lib/types/setup.d.ts
CHANGED
|
@@ -56,17 +56,24 @@ export declare function credentialsPath(home: string): string;
|
|
|
56
56
|
/** The settings document path under a harness home. */
|
|
57
57
|
export declare function settingsPath(home: string): string;
|
|
58
58
|
/**
|
|
59
|
-
* Read one credential reference from a
|
|
59
|
+
* Read one credential reference from a versioned or pre-release flat document.
|
|
60
60
|
* @param path - the credentials document path.
|
|
61
|
-
* @param keyEnv - the
|
|
62
|
-
* @returns the stored value, or `undefined` when
|
|
61
|
+
* @param keyEnv - the reference name to read (e.g. `SILICONFLOW_API_KEY`).
|
|
62
|
+
* @returns the stored value, or `undefined` when the reference is absent, the
|
|
63
|
+
* file is missing, or the document is not a recognizable layout.
|
|
63
64
|
*/
|
|
64
65
|
export declare function readCredential(path: string, keyEnv: string): Promise<string | undefined>;
|
|
65
66
|
/**
|
|
66
|
-
*
|
|
67
|
+
* Write one credential reference into the versioned layout, preserving every
|
|
68
|
+
* other entry and comment. A pre-release flat document is upgraded in place,
|
|
69
|
+
* and a top-level key a pre-fix release of this wizard left beside `version`
|
|
70
|
+
* is folded back under `refs`, so one run repairs the file its predecessor
|
|
71
|
+
* corrupted. An unrecognized document fails loud instead of being rewritten —
|
|
72
|
+
* a silent rewrite would hide why the running harness rejects it.
|
|
67
73
|
* @param path - the credentials document path; created when absent.
|
|
68
|
-
* @param keyEnv - the
|
|
74
|
+
* @param keyEnv - the reference name to write.
|
|
69
75
|
* @param key - the value.
|
|
76
|
+
* @throws when the document exists but is not a recognizable credentials layout.
|
|
70
77
|
*/
|
|
71
78
|
export declare function writeCredential(path: string, keyEnv: string, key: string): Promise<void>;
|
|
72
79
|
/**
|
package/lib/types/types.d.ts
CHANGED
|
@@ -31,10 +31,22 @@ export interface WireSystemMessage {
|
|
|
31
31
|
role: 'system';
|
|
32
32
|
content: string;
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
34
|
+
/** One text part in a multipart user message. */
|
|
35
|
+
export interface WireTextPart {
|
|
36
|
+
type: 'text';
|
|
37
|
+
text: string;
|
|
38
|
+
}
|
|
39
|
+
/** One image part in a multipart user message (OpenAI-compatible `image_url`). */
|
|
40
|
+
export interface WireImagePart {
|
|
41
|
+
type: 'image_url';
|
|
42
|
+
image_url: {
|
|
43
|
+
url: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** A user-role message: a plain string for text-only, or an array of content parts for multimodal. */
|
|
35
47
|
export interface WireUserMessage {
|
|
36
48
|
role: 'user';
|
|
37
|
-
content: string;
|
|
49
|
+
content: string | (WireTextPart | WireImagePart)[];
|
|
38
50
|
}
|
|
39
51
|
/** Tool-role message: the result of one tool call, keyed by its call id. */
|
|
40
52
|
export interface WireToolMessage {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE,
|
|
2
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, RetryPolicySchema, ToolCallId, assertUsableApiKey, attributionHeaders, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
4
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
5
|
-
import { deepEqualJson
|
|
5
|
+
import { deepEqualJson } from "@deepseek-ai/dsh-util-values";
|
|
6
6
|
import { MAX_TIMER_DELAY_MS, idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
|
|
7
7
|
import { getOrCreateAnonymousUserId } from "@deepseek-ai/dsh-anonymous-user-id";
|
|
8
8
|
import { EventSourceParserStream } from "eventsource-parser/stream";
|
|
@@ -158,20 +158,79 @@ async function discoverChatModels(baseURL, apiKey, signal) {
|
|
|
158
158
|
* joined; assistant text becomes `content`, tool calls become `tool_calls`,
|
|
159
159
|
* and tool results become separate tool messages. Assistant reasoning is
|
|
160
160
|
* replayed as `reasoning_content` only on tool-call turns, as hosted reasoning
|
|
161
|
-
* models (DeepSeek-R1 and siblings) require.
|
|
162
|
-
*
|
|
163
|
-
*
|
|
161
|
+
* models (DeepSeek-R1 and siblings) require.
|
|
162
|
+
*
|
|
163
|
+
* **Multimodal**: image blocks in user messages are serialized as
|
|
164
|
+
* OpenAI-compatible `image_url` content parts with base64 data URLs. The
|
|
165
|
+
* serializer resolves image attachments through the optional attachment-store
|
|
166
|
+
* resolver; when no resolver is available, images are replaced with the
|
|
167
|
+
* `OFFLOADED_IMAGE_TEXT` sentinel. Tool-result content is flattened to text
|
|
168
|
+
* (images within tool results are also replaced with the sentinel). Unknown
|
|
169
|
+
* declaration-merged block types retain the adapter's documented extension
|
|
170
|
+
* fallback.
|
|
171
|
+
*
|
|
164
172
|
* @module dsh-llm-siliconflow/serialize
|
|
165
173
|
*/
|
|
174
|
+
/**
|
|
175
|
+
* Model-facing stand-in for an image that could not be resolved to a data URL.
|
|
176
|
+
* Mirrors the upstream `OFFLOADED_IMAGE_TEXT` sentinel from `@deepseek-ai/dsh-llm`
|
|
177
|
+
* (available since 0.1.1-rc.1); defined locally so the plugin works against
|
|
178
|
+
* the 0.1.0-rc.x line pinned in CI as well.
|
|
179
|
+
*/
|
|
180
|
+
const OFFLOADED_IMAGE_TEXT = "[image omitted to keep the request within its image limit; older images are omitted first. If this image is still needed, read its file again when a path is available; otherwise ask the user to attach it again.]";
|
|
181
|
+
/** Default image resolver: returns undefined (no data URL available). */
|
|
182
|
+
const noopResolveImage = () => Promise.resolve(void 0);
|
|
166
183
|
/** Join the text blocks of a message (used for user/tool-result content). */
|
|
167
184
|
function flattenText(blocks) {
|
|
168
185
|
return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
169
186
|
}
|
|
170
|
-
/**
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Replace image blocks (including nested ones in tool results) with the
|
|
189
|
+
* `OFFLOADED_IMAGE_TEXT` sentinel so the provider sees a coherent text
|
|
190
|
+
* placeholder instead of silently dropped bytes. This is the no-store fallback;
|
|
191
|
+
* the multimodal path resolves real base64 data URLs instead.
|
|
192
|
+
*/
|
|
193
|
+
function replaceImagesWithSentinel(blocks) {
|
|
194
|
+
return blocks.map((block) => block.type === "image" ? {
|
|
195
|
+
type: "text",
|
|
196
|
+
text: OFFLOADED_IMAGE_TEXT
|
|
197
|
+
} : block);
|
|
198
|
+
}
|
|
199
|
+
/** Whether this message's content has any image blocks (user side). */
|
|
200
|
+
function hasImages(blocks) {
|
|
201
|
+
return blocks.some((block) => block.type === "image");
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Build the wire content parts for a user message: text blocks become `text`
|
|
205
|
+
* parts, image blocks become `image_url` parts. Non-text/non-image blocks are
|
|
206
|
+
* skipped (merge-extensible fallback). An image block whose attachment cannot
|
|
207
|
+
* be resolved becomes the `OFFLOADED_IMAGE_TEXT` sentinel text part.
|
|
208
|
+
*/
|
|
209
|
+
async function serializeUserContent(blocks, resolveImage) {
|
|
210
|
+
if (!hasImages(blocks)) return flattenText(blocks);
|
|
211
|
+
const parts = [];
|
|
212
|
+
for (const block of blocks) if (block.type === "text") parts.push({
|
|
213
|
+
type: "text",
|
|
214
|
+
text: block.text
|
|
215
|
+
});
|
|
216
|
+
else if (block.type === "image") {
|
|
217
|
+
const url = await resolveImage(block.attachment);
|
|
218
|
+
parts.push(url !== void 0 ? {
|
|
219
|
+
type: "image_url",
|
|
220
|
+
image_url: { url }
|
|
221
|
+
} : {
|
|
222
|
+
type: "text",
|
|
223
|
+
text: OFFLOADED_IMAGE_TEXT
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return parts;
|
|
173
227
|
}
|
|
174
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* Serialize one assistant message (text + reasoning + tool calls). Image
|
|
230
|
+
* blocks in assistant content (forward compatibility) are replaced with the
|
|
231
|
+
* sentinel text — the chat-completions wire route carries images only in user
|
|
232
|
+
* content.
|
|
233
|
+
*/
|
|
175
234
|
function serializeAssistant(message) {
|
|
176
235
|
const text = flattenText(message.content);
|
|
177
236
|
const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
|
|
@@ -195,13 +254,17 @@ function serializeAssistant(message) {
|
|
|
195
254
|
* `{role: 'tool'}` messages; the harness puts each tool result in its own
|
|
196
255
|
* user-role message, so a mixed user message contributes its text first and
|
|
197
256
|
* its tool results as separate wire messages after.
|
|
257
|
+
*
|
|
258
|
+
* Image blocks within user messages are resolved through the `resolveImage`
|
|
259
|
+
* callback; images within tool results are flattened to the sentinel text
|
|
260
|
+
* (tool results carry structured data, not multimodal content).
|
|
198
261
|
* @param messages - the harness conversation, in order.
|
|
262
|
+
* @param resolveImage - resolves an image attachment ref to a base64 data URL; returns `undefined` when unavailable.
|
|
199
263
|
* @returns the wire messages; order preserved, each tool result expanded into its own entry.
|
|
200
264
|
*/
|
|
201
|
-
function serializeMessages(messages) {
|
|
265
|
+
async function serializeMessages(messages, resolveImage = noopResolveImage) {
|
|
202
266
|
const wire = [];
|
|
203
267
|
for (const message of messages) {
|
|
204
|
-
assertTextOnly(message.content);
|
|
205
268
|
if (message.role === "system") {
|
|
206
269
|
wire.push({
|
|
207
270
|
role: "system",
|
|
@@ -214,16 +277,22 @@ function serializeMessages(messages) {
|
|
|
214
277
|
continue;
|
|
215
278
|
}
|
|
216
279
|
const toolResults = message.content.filter((block) => block.type === "tool-result");
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
280
|
+
const userBlocks = message.content.filter((block) => block.type !== "tool-result");
|
|
281
|
+
if (userBlocks.length > 0 || toolResults.length === 0) {
|
|
282
|
+
const content = await serializeUserContent(userBlocks, resolveImage);
|
|
283
|
+
wire.push({
|
|
284
|
+
role: "user",
|
|
285
|
+
content
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
for (const result of toolResults) {
|
|
289
|
+
const flatContent = flattenText(replaceImagesWithSentinel(result.content)) || "(no output)";
|
|
290
|
+
wire.push({
|
|
291
|
+
role: "tool",
|
|
292
|
+
tool_call_id: result.toolCallId,
|
|
293
|
+
content: flatContent
|
|
294
|
+
});
|
|
295
|
+
}
|
|
227
296
|
}
|
|
228
297
|
return wire;
|
|
229
298
|
}
|
|
@@ -232,15 +301,16 @@ function serializeMessages(messages) {
|
|
|
232
301
|
* reporting on); optional fields are omitted rather than sent as null, so
|
|
233
302
|
* provider defaults apply.
|
|
234
303
|
* @param options - the harness request (model, history, system, tools, sampling).
|
|
304
|
+
* @param resolveImage - resolves an image attachment ref to a base64 data URL; returns `undefined` when unavailable.
|
|
235
305
|
* @returns the chat-completions request body.
|
|
236
306
|
*/
|
|
237
|
-
function serializeRequest(options) {
|
|
307
|
+
async function serializeRequest(options, resolveImage = noopResolveImage) {
|
|
238
308
|
const messages = [];
|
|
239
309
|
if (options.system !== void 0) messages.push({
|
|
240
310
|
role: "system",
|
|
241
311
|
content: options.system
|
|
242
312
|
});
|
|
243
|
-
messages.push(...serializeMessages(options.messages));
|
|
313
|
+
messages.push(...await serializeMessages(options.messages, resolveImage));
|
|
244
314
|
const tools = options.tools?.map((tool) => ({
|
|
245
315
|
type: "function",
|
|
246
316
|
function: {
|
|
@@ -338,7 +408,7 @@ function closeBlock(block) {
|
|
|
338
408
|
};
|
|
339
409
|
case "tool-call": return {
|
|
340
410
|
type: "tool-call",
|
|
341
|
-
id:
|
|
411
|
+
id: ToolCallId(block.callId ?? ""),
|
|
342
412
|
name: block.name ?? "",
|
|
343
413
|
arguments: block.text
|
|
344
414
|
};
|
|
@@ -453,7 +523,7 @@ async function* translate(payloads) {
|
|
|
453
523
|
yield {
|
|
454
524
|
type: "tool-call-delta",
|
|
455
525
|
index: block.index,
|
|
456
|
-
id:
|
|
526
|
+
id: ToolCallId(block.callId ?? ""),
|
|
457
527
|
...block.name !== void 0 ? { name: block.name } : {},
|
|
458
528
|
argumentsDelta: fragment
|
|
459
529
|
};
|
|
@@ -542,13 +612,71 @@ const DEFAULT_MAX_TOKENS = 8192;
|
|
|
542
612
|
/** How long a cached model-listing discovery stays fresh before the next `listModels` re-interrogates. */
|
|
543
613
|
const DISCOVERY_TTL_MS = 3e5;
|
|
544
614
|
const STREAM_IDLE_TIMEOUT_CODE = "LLM_STREAM_IDLE_TIMEOUT";
|
|
615
|
+
/**
|
|
616
|
+
* Authoritative set of SiliconFlow model ids that accept image input.
|
|
617
|
+
*
|
|
618
|
+
* **Static data — see maintenance note below.**
|
|
619
|
+
*
|
|
620
|
+
* SiliconFlow's model marketplace (siliconflow.cn/models) tags every chat model
|
|
621
|
+
* with a `vlm` boolean. The OpenAI-compatible `GET /models` API does not expose
|
|
622
|
+
* this attribute, so the adapter ships a set of known VLM model ids extracted
|
|
623
|
+
* from the marketplace's SSR data. Catalog entries can still declare explicit
|
|
624
|
+
* `inputModalities` to override this set.
|
|
625
|
+
*
|
|
626
|
+
* **Maintenance**: this set is hand-maintained and will drift from the live
|
|
627
|
+
* marketplace as new VLM models are added or existing ones are deprecated. It is
|
|
628
|
+
* refreshed periodically. Once the OpenAI-compatible API exposes model
|
|
629
|
+
* capabilities (e.g. a `vision` sub_type or a `vlm` field in the model listing),
|
|
630
|
+
* this static set will be replaced by a runtime query.
|
|
631
|
+
*
|
|
632
|
+
* Last updated: 2026-08-22 (23 VLM models out of 88 chat models).
|
|
633
|
+
*/
|
|
634
|
+
const KNOWN_VLM_MODELS = /* @__PURE__ */ new Set([
|
|
635
|
+
"PaddlePaddle/PaddleOCR-VL-1.5",
|
|
636
|
+
"Pro/moonshotai/Kimi-K2.6",
|
|
637
|
+
"Qwen/Qwen3-Omni-30B-A3B-Captioner",
|
|
638
|
+
"Qwen/Qwen3-Omni-30B-A3B-Instruct",
|
|
639
|
+
"Qwen/Qwen3-Omni-30B-A3B-Thinking",
|
|
640
|
+
"Qwen/Qwen3-VL-30B-A3B-Instruct",
|
|
641
|
+
"Qwen/Qwen3-VL-30B-A3B-Thinking",
|
|
642
|
+
"Qwen/Qwen3-VL-32B-Instruct",
|
|
643
|
+
"Qwen/Qwen3-VL-32B-Thinking",
|
|
644
|
+
"Qwen/Qwen3-VL-8B-Instruct",
|
|
645
|
+
"Qwen/Qwen3-VL-8B-Thinking",
|
|
646
|
+
"Qwen/Qwen3.5-122B-A10B",
|
|
647
|
+
"Qwen/Qwen3.5-27B",
|
|
648
|
+
"Qwen/Qwen3.5-35B-A3B",
|
|
649
|
+
"Qwen/Qwen3.5-397B-A17B",
|
|
650
|
+
"Qwen/Qwen3.5-4B",
|
|
651
|
+
"Qwen/Qwen3.5-9B",
|
|
652
|
+
"Qwen/Qwen3.6-27B",
|
|
653
|
+
"Qwen/Qwen3.6-35B-A3B",
|
|
654
|
+
"deepseek-ai/DeepSeek-OCR",
|
|
655
|
+
"moonshotai/Kimi-K2.7-Code",
|
|
656
|
+
"nex-agi/Nex-N2-Pro",
|
|
657
|
+
"zai-org/GLM-4.5V"
|
|
658
|
+
]);
|
|
659
|
+
/**
|
|
660
|
+
* Determine the input modalities for a SiliconFlow model id.
|
|
661
|
+
*
|
|
662
|
+
* Uses the authoritative VLM model set from the SiliconFlow marketplace rather
|
|
663
|
+
* than naming-pattern heuristics. Catalog entries can declare explicit
|
|
664
|
+
* `inputModalities` to override this lookup.
|
|
665
|
+
* @param id - the wire model id (e.g. `Qwen/Qwen3-VL-8B-Instruct`).
|
|
666
|
+
* @returns `['text', 'image']` when the id is a known VLM, `['text']` otherwise.
|
|
667
|
+
*/
|
|
668
|
+
function inferInputModalities(id) {
|
|
669
|
+
return KNOWN_VLM_MODELS.has(id) ? ["text", "image"] : ["text"];
|
|
670
|
+
}
|
|
545
671
|
function modelInfo(provider, model) {
|
|
672
|
+
const explicit = model.inputModalities;
|
|
673
|
+
const modalities = explicit !== void 0 && explicit.length > 0 ? explicit : inferInputModalities(model.id);
|
|
546
674
|
return {
|
|
547
675
|
provider,
|
|
548
676
|
id: model.id,
|
|
549
677
|
name: model.name ?? model.id,
|
|
550
678
|
...model.description === void 0 ? {} : { description: model.description },
|
|
551
|
-
inputModalities:
|
|
679
|
+
inputModalities: modalities
|
|
552
680
|
};
|
|
553
681
|
}
|
|
554
682
|
function providerRetryAfterMs(value) {
|
|
@@ -655,7 +783,7 @@ var SiliconFlowAdapter = class extends LlmAdapter {
|
|
|
655
783
|
provider,
|
|
656
784
|
id: model,
|
|
657
785
|
name: model,
|
|
658
|
-
inputModalities:
|
|
786
|
+
inputModalities: inferInputModalities(model)
|
|
659
787
|
} : modelInfo(provider, entry),
|
|
660
788
|
context: { contextWindow: entry?.contextWindow ?? connection.defaultContextWindow },
|
|
661
789
|
defaultMaxTokens: entry?.maxTokens ?? connection.maxTokens
|
|
@@ -706,7 +834,7 @@ var SiliconFlowAdapter = class extends LlmAdapter {
|
|
|
706
834
|
}
|
|
707
835
|
}
|
|
708
836
|
async *request(options, signal, connection, apiKey, userId, onComment) {
|
|
709
|
-
const body = serializeRequest(options);
|
|
837
|
+
const body = await serializeRequest(options, this.config.resolveImage);
|
|
710
838
|
const payload = JSON.stringify(body);
|
|
711
839
|
const headers = {
|
|
712
840
|
"authorization": `Bearer ${apiKey}`,
|
|
@@ -760,25 +888,36 @@ var SiliconFlowAdapter = class extends LlmAdapter {
|
|
|
760
888
|
* restarting anything, while an in-flight stream keeps the facts it started
|
|
761
889
|
* with. The one registration-captured fact — the retry policy — re-registers
|
|
762
890
|
* the route in place when it changes.
|
|
891
|
+
*
|
|
892
|
+
* When the optional `ctx.attachments` service is mounted, image blocks in user
|
|
893
|
+
* messages are resolved to base64 data URLs and serialized as OpenAI-compatible
|
|
894
|
+
* `image_url` content parts, enabling VLM models (Qwen3-VL, GLM-4.5V, etc.).
|
|
763
895
|
* @module @siliconflow-official/dsh-llm-siliconflow
|
|
764
896
|
*/
|
|
765
897
|
const name = "llm-siliconflow";
|
|
766
898
|
const inject = ["llm"];
|
|
767
|
-
const NS =
|
|
899
|
+
const NS = "llm-siliconflow";
|
|
768
900
|
/** Credential reference this plugin reads by default, also used by the setup CLI. */
|
|
769
901
|
const DEFAULT_API_KEY_ENV = "SILICONFLOW_API_KEY";
|
|
770
902
|
/** The single provider route this plugin owns. */
|
|
771
903
|
const PROVIDER = "siliconflow";
|
|
772
|
-
/**
|
|
904
|
+
/**
|
|
905
|
+
* Fallback advisory catalog: widely hosted chat models including VLMs, also the
|
|
906
|
+
* setup CLI's discovery fallback.
|
|
907
|
+
*
|
|
908
|
+
* **Static data** — the `contextWindow` values and VLM flags are extracted from
|
|
909
|
+
* the SiliconFlow model marketplace (siliconflow.cn/models). The live
|
|
910
|
+
* `GET /models?sub_type=chat` API does not return context window or VLM metadata,
|
|
911
|
+
* so these fields are maintained by hand. This catalog will be kept in sync with
|
|
912
|
+
* the marketplace until the API exposes these attributes natively.
|
|
913
|
+
*
|
|
914
|
+
* Last updated: 2026-08-22. VLM entries declare `inputModalities` explicitly.
|
|
915
|
+
*/
|
|
773
916
|
const DEFAULT_MODELS = [
|
|
774
917
|
{
|
|
775
918
|
id: "zai-org/GLM-5.2",
|
|
776
919
|
contextWindow: 1e6
|
|
777
920
|
},
|
|
778
|
-
{
|
|
779
|
-
id: "moonshotai/Kimi-K2.7-Code",
|
|
780
|
-
contextWindow: 256e3
|
|
781
|
-
},
|
|
782
921
|
{
|
|
783
922
|
id: "deepseek-ai/DeepSeek-V4-Pro",
|
|
784
923
|
contextWindow: 1e6
|
|
@@ -787,13 +926,54 @@ const DEFAULT_MODELS = [
|
|
|
787
926
|
id: "deepseek-ai/DeepSeek-V4-Flash",
|
|
788
927
|
contextWindow: 1e6
|
|
789
928
|
},
|
|
929
|
+
{
|
|
930
|
+
id: "Pro/zai-org/GLM-5.1",
|
|
931
|
+
contextWindow: 202752
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
id: "moonshotai/Kimi-K2.7-Code",
|
|
935
|
+
contextWindow: 262144,
|
|
936
|
+
inputModalities: ["text", "image"]
|
|
937
|
+
},
|
|
790
938
|
{
|
|
791
939
|
id: "Pro/moonshotai/Kimi-K2.6",
|
|
792
|
-
contextWindow:
|
|
940
|
+
contextWindow: 262144,
|
|
941
|
+
inputModalities: ["text", "image"]
|
|
793
942
|
},
|
|
794
943
|
{
|
|
795
944
|
id: "Qwen/Qwen3.5-397B-A17B",
|
|
796
|
-
contextWindow:
|
|
945
|
+
contextWindow: 262144,
|
|
946
|
+
inputModalities: ["text", "image"]
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
id: "zai-org/GLM-4.5V",
|
|
950
|
+
contextWindow: 65536,
|
|
951
|
+
inputModalities: ["text", "image"]
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
id: "Qwen/Qwen3-VL-32B-Instruct",
|
|
955
|
+
contextWindow: 262144,
|
|
956
|
+
inputModalities: ["text", "image"]
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
id: "Qwen/Qwen3-VL-8B-Instruct",
|
|
960
|
+
contextWindow: 262144,
|
|
961
|
+
inputModalities: ["text", "image"]
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
id: "Qwen/Qwen3-VL-32B-Thinking",
|
|
965
|
+
contextWindow: 262144,
|
|
966
|
+
inputModalities: ["text", "image"]
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
id: "Qwen/Qwen3-VL-8B-Thinking",
|
|
970
|
+
contextWindow: 262144,
|
|
971
|
+
inputModalities: ["text", "image"]
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
id: "deepseek-ai/DeepSeek-OCR",
|
|
975
|
+
contextWindow: 8192,
|
|
976
|
+
inputModalities: ["text", "image"]
|
|
797
977
|
}
|
|
798
978
|
];
|
|
799
979
|
const catalogModel = z.object({
|
|
@@ -801,7 +981,8 @@ const catalogModel = z.object({
|
|
|
801
981
|
name: z.string(),
|
|
802
982
|
description: z.string(),
|
|
803
983
|
contextWindow: z.number().step(1).min(1),
|
|
804
|
-
maxTokens: z.number().step(1).min(1)
|
|
984
|
+
maxTokens: z.number().step(1).min(1),
|
|
985
|
+
inputModalities: z.array(z.union(["text", "image"]))
|
|
805
986
|
});
|
|
806
987
|
const Config = z.object({
|
|
807
988
|
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
@@ -831,7 +1012,8 @@ function resolveModels(models) {
|
|
|
831
1012
|
...model.name === void 0 ? {} : { name: model.name },
|
|
832
1013
|
...model.description === void 0 ? {} : { description: model.description },
|
|
833
1014
|
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
834
|
-
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens }
|
|
1015
|
+
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
|
|
1016
|
+
...model.inputModalities === void 0 || model.inputModalities.length === 0 ? {} : { inputModalities: model.inputModalities }
|
|
835
1017
|
};
|
|
836
1018
|
});
|
|
837
1019
|
}
|
|
@@ -895,6 +1077,22 @@ function apply(ctx, config) {
|
|
|
895
1077
|
}
|
|
896
1078
|
throw new LlmError(`llm-siliconflow: 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");
|
|
897
1079
|
};
|
|
1080
|
+
/**
|
|
1081
|
+
* Resolve one image attachment to a base64 data URL for wire serialization.
|
|
1082
|
+
* Uses the optional `ctx.attachments` service; returns `undefined` when the
|
|
1083
|
+
* store is not mounted or the read fails, so the serializer substitutes the
|
|
1084
|
+
* `OFFLOADED_IMAGE_TEXT` sentinel instead of blocking the request.
|
|
1085
|
+
*/
|
|
1086
|
+
const resolveImage = async (ref) => {
|
|
1087
|
+
const attachments = ctx.get("attachments");
|
|
1088
|
+
if (attachments === void 0) return void 0;
|
|
1089
|
+
try {
|
|
1090
|
+
const { data } = await attachments.readImage(ref);
|
|
1091
|
+
return `data:${ref.mediaType};base64,${Buffer.from(data).toString("base64")}`;
|
|
1092
|
+
} catch {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
898
1096
|
let userId;
|
|
899
1097
|
const resolveUserId = () => userId ??= getOrCreateAnonymousUserId();
|
|
900
1098
|
const storedApiKey = async () => {
|
|
@@ -907,7 +1105,8 @@ function apply(ctx, config) {
|
|
|
907
1105
|
const adapter = new SiliconFlowAdapter({
|
|
908
1106
|
options,
|
|
909
1107
|
resolveApiKey,
|
|
910
|
-
resolveUserId
|
|
1108
|
+
resolveUserId,
|
|
1109
|
+
resolveImage
|
|
911
1110
|
});
|
|
912
1111
|
ctx.llm.registerConfigurableProviders([{
|
|
913
1112
|
provider: PROVIDER,
|
|
@@ -926,12 +1125,14 @@ function apply(ctx, config) {
|
|
|
926
1125
|
registration.replace([PROVIDER]);
|
|
927
1126
|
registeredPolicy = policy;
|
|
928
1127
|
};
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1128
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
1129
|
+
settingsCtx.settings.installSection(ctx, NS, Config, config, {
|
|
1130
|
+
setSource: (source) => {
|
|
1131
|
+
current = source;
|
|
1132
|
+
},
|
|
1133
|
+
onChange: ensureRegistrationFacts
|
|
1134
|
+
});
|
|
934
1135
|
});
|
|
935
1136
|
}
|
|
936
1137
|
//#endregion
|
|
937
|
-
export {
|
|
1138
|
+
export { listingUrl as _, PUBLIC_BASE_URL as a, name as c, DEFAULT_MAX_TOKENS as d, DEFAULT_STREAM_IDLE_TIMEOUT_MS as f, discoverChatModels as g, inferInputModalities as h, PROVIDER as i, resolveAdapterOptions as l, SiliconFlowAdapter as m, DEFAULT_API_KEY_ENV as n, apply as o, DISCOVERY_TTL_MS as p, DEFAULT_MODELS as r, inject as s, Config as t, DEFAULT_CONTEXT_WINDOW as u, readListing as v };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siliconflow-official/dsh-llm-siliconflow",
|
|
3
3
|
"description": "SiliconFlow (OpenAI-compatible) chat-completions adapter plugin for the DeepSeek Harness LLM seam",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0-rc.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -45,12 +45,14 @@
|
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
47
47
|
"@deepseek-ai/dsh-anonymous-user-id": ">=0.0.1-rc.0",
|
|
48
|
+
"@deepseek-ai/dsh-attachment": ">=0.0.1-rc.0",
|
|
48
49
|
"@deepseek-ai/dsh-credentials": ">=0.0.1-rc.0",
|
|
49
50
|
"@deepseek-ai/dsh-invariants": ">=0.0.1-rc.0",
|
|
50
51
|
"@deepseek-ai/dsh-launch-environment": ">=0.0.1-rc.0",
|
|
51
52
|
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.0",
|
|
52
53
|
"@deepseek-ai/dsh-settings": ">=0.0.1-rc.0",
|
|
53
|
-
"@deepseek-ai/dsh-timeout": ">=0.0.1-rc.0"
|
|
54
|
+
"@deepseek-ai/dsh-timeout": ">=0.0.1-rc.0",
|
|
55
|
+
"@deepseek-ai/dsh-util-values": "*"
|
|
54
56
|
},
|
|
55
57
|
"dependencies": {
|
|
56
58
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
@@ -58,14 +60,16 @@
|
|
|
58
60
|
"yaml": "^2.9.0"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
61
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
62
|
-
"@deepseek-ai/dsh-anonymous-user-id": "
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-
|
|
66
|
-
"@deepseek-ai/dsh-
|
|
67
|
-
"@deepseek-ai/dsh-
|
|
68
|
-
"@deepseek-ai/dsh-
|
|
63
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
64
|
+
"@deepseek-ai/dsh-anonymous-user-id": "0.1.5-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-credentials": "0.1.5-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-invariants": "0.1.5-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-launch-environment": "0.1.5-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-timeout": "0.1.5-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-util-values": "0.1.5-rc.2",
|
|
69
73
|
"@types/node": "^22.0.0",
|
|
70
74
|
"tsdown": "^0.22.0",
|
|
71
75
|
"typescript": "^6.0.0"
|