@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 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
- 插件把单个提供方路由 `siliconflow` 连同其已解析的 `retryPolicy` 一并注册。请求用 `provider: siliconflow` 选中它;其 `model` 原样作为线上 `model` 字符串透传,因此更换 SiliconFlow 模型不需要生命周期级重新注册。线上模型 id 是 SiliconFlow 的 `org/model` 写法(如 `deepseek-ai/DeepSeek-V4-Flash`),绝不是短别名。省略 `models` 时保留一份由六个当前托管对话模型组成的小回退目录;显式列表会替换这些默认值,而 `models: []` 则一个都不通告。目录条目通过 `ctx.llm.listModels('siliconflow')` 暴露给 ACP 编辑器与 Web 选择器这类客户端,但始终是建议性的:未列出的模型 id 依然原样透传。省略的条目名默认等于其 id。
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
- - **图片内容被拒绝** —— 这里的 chat-completions 线上路由仅文本;多模态 SiliconFlow 路由需要自己的内容序列化器。
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, h as discoverChatModels, i as PROVIDER, n as DEFAULT_API_KEY_ENV, r as DEFAULT_MODELS } from "./types-CmhCs_oH.js";
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
- * Read one credential reference from a comment-preserving document.
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 top-level reference name (e.g. `SILICONFLOW_API_KEY`).
52
- * @returns the stored value, or `undefined` when absent or the file is missing.
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 value = parseDocument(text).toJS()?.[keyEnv];
63
- return typeof value === "string" && value.length > 0 ? value : void 0;
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
- * Set one credential reference, preserving every other entry and comment.
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 top-level reference name to write.
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.set(keyEnv, key);
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 readListing, a as PUBLIC_BASE_URL, c as name, d as DEFAULT_MAX_TOKENS, f as DEFAULT_STREAM_IDLE_TIMEOUT_MS, g as listingUrl, h as discoverChatModels, 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 } from "./types-CmhCs_oH.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, 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 };
@@ -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.
@@ -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
- /** Fallback advisory catalog: six widely hosted chat models, also the setup CLI's discovery fallback. */
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 six widely hosted models. */
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;
@@ -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. Core image blocks are rejected
7
- * explicitly because this wire route is text-only; unknown declaration-merged
8
- * block types retain the adapter's documented extension fallback.
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
@@ -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 comment-preserving document.
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 top-level reference name (e.g. `SILICONFLOW_API_KEY`).
62
- * @returns the stored value, or `undefined` when absent or the file is missing.
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
- * Set one credential reference, preserving every other entry and comment.
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 top-level reference name to write.
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
  /**
@@ -31,10 +31,22 @@ export interface WireSystemMessage {
31
31
  role: 'system';
32
32
  content: string;
33
33
  }
34
- /** User-role message: a single string of user input. */
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, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, RetryPolicySchema, assertUsableApiKey, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
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, installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
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. Core image blocks are rejected
162
- * explicitly because this wire route is text-only; unknown declaration-merged
163
- * block types retain the adapter's documented extension fallback.
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
- /** Reject core image content before any text-flattening path can silently erase it. */
171
- function assertTextOnly(blocks) {
172
- if (contentHasImage(blocks)) throw new LlmError("The SiliconFlow chat-completions adapter does not support image content.", "UNSUPPORTED_CONTENT");
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
- /** Serialize one assistant message (text + reasoning + tool calls). */
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 text = flattenText(message.content);
218
- if (text.length > 0 || toolResults.length === 0) wire.push({
219
- role: "user",
220
- content: text
221
- });
222
- for (const result of toolResults) wire.push({
223
- role: "tool",
224
- tool_call_id: result.toolCallId,
225
- content: flattenText(result.content) || "(no output)"
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: CallId(block.callId ?? ""),
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: CallId(block.callId ?? ""),
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: ["text"]
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: ["text"]
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 = settingsNamespace("llm-siliconflow");
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
- /** Fallback advisory catalog: six widely hosted chat models, also the setup CLI's discovery fallback. */
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: 256e3
940
+ contextWindow: 262144,
941
+ inputModalities: ["text", "image"]
793
942
  },
794
943
  {
795
944
  id: "Qwen/Qwen3.5-397B-A17B",
796
- contextWindow: 256e3
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
- installSettingsSection(ctx, NS, Config, config, {
930
- setSource: (source) => {
931
- current = source;
932
- },
933
- onChange: ensureRegistrationFacts
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 { readListing as _, PUBLIC_BASE_URL as a, name as c, DEFAULT_MAX_TOKENS as d, DEFAULT_STREAM_IDLE_TIMEOUT_MS as f, listingUrl as g, discoverChatModels 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 };
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.1.0-rc.7",
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.1",
62
- "@deepseek-ai/dsh-anonymous-user-id": ">=0.0.1-rc.0",
63
- "@deepseek-ai/dsh-credentials": ">=0.0.1-rc.0",
64
- "@deepseek-ai/dsh-invariants": ">=0.0.1-rc.0",
65
- "@deepseek-ai/dsh-launch-environment": ">=0.0.1-rc.0",
66
- "@deepseek-ai/dsh-llm": ">=0.0.1-rc.0",
67
- "@deepseek-ai/dsh-settings": ">=0.0.1-rc.0",
68
- "@deepseek-ai/dsh-timeout": ">=0.0.1-rc.0",
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"