@dsh-plus/llm-pi 0.1.13 → 0.1.14

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/src/compat.ts CHANGED
@@ -1,10 +1,25 @@
1
1
  /**
2
- * 逐协议 compat 字段表与校验。
2
+ * 逐协议 compat 门控表与校验(0.1.2-alpha.1 适配版)。
3
3
  *
4
- * 背景:官方 llm-pi-ai 的配置路径只物化 thinkingFormat/supportsReasoningEffort
5
- * 两个字段,多余键被静默丢弃;本插件开放 pi-ai 的全量 compat(字段表与
6
- * pi-ai@0.82.1 types.d.ts:423-538 逐字段对齐),并在 settings 写入与
7
- * profile 构建时校验——未知键/错类型值直接拒绝并给出合法键清单。
4
+ * 适配决策:官方 dsh-llm-pi-ai 0.1.2-alpha.1 的 COMPAT_GATES(catalog.ts:292)
5
+ * compat 可配性的唯一事实源——按协议分型(offer/withhold),withhold 字段
6
+ * (catalog 已为对应厂商设置,如 openRouterRouting/zaiToolStream/
7
+ * sendSessionAffinityHeaders/supportsToolSearch 等)写时拒绝并提示以目录
8
+ * provider 名为 route。官方门控表只在 src/catalog.ts 源码子路径内(npm 发布
9
+ * 形态不携带 src/、包根也不导出该符号),插件在 dsh 树(npm 布局)与 vendored
10
+ * 兜底两条路径都无法静态引用,故本文件按官方 catalog.ts 逐字段镜像门控表
11
+ * (字段全集与分型随 pi-ai 升级同步维护,官方以 Record<keyof Compat> 编译期
12
+ * 约束漂移,插件以本表 + 注释人工同步)。
13
+ *
14
+ * 与旧版字段表的差异(0.1.2-alpha.1 官方门控):
15
+ * - completions 新增 offer:supportsFinishReason/chatTemplateArgs/
16
+ * supportsThinkingTokenBudget;thinkingFormat 新增 baseten;
17
+ * - 原表内 openRouterRouting/vercelGatewayRouting/zaiToolStream/
18
+ * sendSessionAffinityHeaders/deferredToolsMode/sessionAffinityFormat/
19
+ * supportsOpenAIGrammarTools(completions/responses)与 supportsToolSearch/
20
+ * supportsExplicitPromptCacheMode(responses)、supportsToolReferences
21
+ * (anthropic)改为 withhold → 写时拒绝;
22
+ * - responses 不再 offer sessionAffinityFormat(withhold)。
8
23
  *
9
24
  * pi-ai 侧消费语义:getCompat 逐字段 `??` 覆盖 detectCompat 的
10
25
  * baseURL/名称猜测;undefined 视为未设置(无法显式清空检测值)。
@@ -13,23 +28,103 @@
13
28
  import type { ProtocolId } from './config.ts'
14
29
 
15
30
  type CompatValue = 'boolean' | 'object' | readonly string[]
31
+ type CompatDisposition = 'offer' | 'withhold'
32
+
33
+ /**
34
+ * 官方 COMPLETIONS_COMPAT_GATE(llm-pi-ai/src/catalog.ts)逐字段镜像。
35
+ * offer 17 字段 / withhold 7 字段。
36
+ */
37
+ const COMPLETIONS_COMPAT_GATE: Readonly<Record<string, CompatDisposition>> = {
38
+ supportsStore: 'offer',
39
+ supportsDeveloperRole: 'offer',
40
+ supportsReasoningEffort: 'offer',
41
+ supportsUsageInStreaming: 'offer',
42
+ supportsFinishReason: 'offer',
43
+ maxTokensField: 'offer',
44
+ requiresToolResultName: 'offer',
45
+ requiresAssistantAfterToolResult: 'offer',
46
+ requiresThinkingAsText: 'offer',
47
+ requiresReasoningContentOnAssistantMessages: 'offer',
48
+ thinkingFormat: 'offer',
49
+ chatTemplateKwargs: 'offer',
50
+ chatTemplateArgs: 'offer',
51
+ supportsThinkingTokenBudget: 'offer',
52
+ supportsStrictMode: 'offer',
53
+ cacheControlFormat: 'offer',
54
+ supportsLongCacheRetention: 'offer',
55
+ openRouterRouting: 'withhold',
56
+ vercelGatewayRouting: 'withhold',
57
+ zaiToolStream: 'withhold',
58
+ supportsOpenAIGrammarTools: 'withhold',
59
+ sendSessionAffinityHeaders: 'withhold',
60
+ deferredToolsMode: 'withhold',
61
+ sessionAffinityFormat: 'withhold',
62
+ }
63
+
64
+ /**
65
+ * 官方 RESPONSES_COMPAT_GATE 逐字段镜像(三协议共享同一 OpenAIResponsesCompat;
66
+ * 本插件只服务 openai-responses)。offer 3 字段 / withhold 5 字段。
67
+ */
68
+ const RESPONSES_COMPAT_GATE: Readonly<Record<string, CompatDisposition>> = {
69
+ supportsDeveloperRole: 'offer',
70
+ supportsStrictMode: 'offer',
71
+ supportsLongCacheRetention: 'offer',
72
+ sessionAffinityFormat: 'withhold',
73
+ supportsOpenAIGrammarTools: 'withhold',
74
+ supportsAdditionalTools: 'withhold',
75
+ supportsToolSearch: 'withhold',
76
+ supportsExplicitPromptCacheMode: 'withhold',
77
+ }
78
+
79
+ /**
80
+ * 官方 ANTHROPIC_COMPAT_GATE 逐字段镜像。offer 7 字段 / withhold 2 字段。
81
+ */
82
+ const ANTHROPIC_COMPAT_GATE: Readonly<Record<string, CompatDisposition>> = {
83
+ supportsEagerToolInputStreaming: 'offer',
84
+ supportsLongCacheRetention: 'offer',
85
+ supportsCacheControlOnTools: 'offer',
86
+ supportsTemperature: 'offer',
87
+ forceAdaptiveThinking: 'offer',
88
+ allowEmptySignature: 'offer',
89
+ supportsStrictTools: 'offer',
90
+ sendSessionAffinityHeaders: 'withhold',
91
+ supportsToolReferences: 'withhold',
92
+ }
93
+
94
+ const GATES_BY_PROTOCOL: Record<ProtocolId, Readonly<Record<string, CompatDisposition>>> = {
95
+ 'openai-completions': COMPLETIONS_COMPAT_GATE,
96
+ 'openai-responses': RESPONSES_COMPAT_GATE,
97
+ 'anthropic-messages': ANTHROPIC_COMPAT_GATE,
98
+ }
99
+
100
+ /** 某协议某字段的可配性('offer'/'withhold';未列出 = 无此字段)。 */
101
+ export function compatDispositionOf(api: ProtocolId, field: string): CompatDisposition | undefined {
102
+ return GATES_BY_PROTOCOL[api][field]
103
+ }
16
104
 
17
- /** openai-completions 的 21 个字段(pi-ai OpenAICompletionsCompat)。 */
18
- const COMPLETIONS_FIELDS: Record<string, CompatValue> = {
105
+ /**
106
+ * 字段取值约束(对齐官方 config.ts compatProfile schema):
107
+ * boolean 字段 → 'boolean';maxTokensField/thinkingFormat/cacheControlFormat
108
+ * → 枚举;chatTemplateKwargs/chatTemplateArgs → 对象。
109
+ */
110
+ const VALUE_SPECS: Readonly<Record<string, CompatValue>> = {
19
111
  supportsStore: 'boolean',
20
112
  supportsDeveloperRole: 'boolean',
21
113
  supportsReasoningEffort: 'boolean',
22
114
  supportsUsageInStreaming: 'boolean',
115
+ supportsFinishReason: 'boolean',
23
116
  maxTokensField: ['max_completion_tokens', 'max_tokens'],
24
117
  requiresToolResultName: 'boolean',
25
118
  requiresAssistantAfterToolResult: 'boolean',
26
119
  requiresThinkingAsText: 'boolean',
27
120
  requiresReasoningContentOnAssistantMessages: 'boolean',
28
121
  thinkingFormat: [
122
+ // 官方 SUPPORTED_THINKING_FORMATS(含 baseten,随 chatTemplateArgs 使用)
29
123
  'openai',
30
- 'openrouter',
31
124
  'deepseek',
125
+ 'openrouter',
32
126
  'together',
127
+ 'baseten',
33
128
  'zai',
34
129
  'qwen',
35
130
  'chat-template',
@@ -38,65 +133,32 @@ const COMPLETIONS_FIELDS: Record<string, CompatValue> = {
38
133
  'ant-ling',
39
134
  ],
40
135
  chatTemplateKwargs: 'object',
41
- openRouterRouting: 'object',
42
- vercelGatewayRouting: 'object',
43
- zaiToolStream: 'boolean',
44
- supportsOpenAIGrammarTools: 'boolean',
136
+ chatTemplateArgs: 'object',
137
+ supportsThinkingTokenBudget: 'boolean',
45
138
  supportsStrictMode: 'boolean',
46
139
  cacheControlFormat: ['anthropic'],
47
- sendSessionAffinityHeaders: 'boolean',
48
- deferredToolsMode: ['kimi'],
49
- sessionAffinityFormat: ['openai', 'openai-nosession', 'openrouter'],
50
140
  supportsLongCacheRetention: 'boolean',
51
- }
52
-
53
- /** openai-responses 的 7 个字段(pi-ai OpenAIResponsesCompat)。 */
54
- const RESPONSES_FIELDS: Record<string, CompatValue> = {
55
- supportsDeveloperRole: 'boolean',
56
- sessionAffinityFormat: ['openai', 'openai-nosession', 'openrouter'],
57
- supportsLongCacheRetention: 'boolean',
58
- supportsStrictMode: 'boolean',
59
- supportsOpenAIGrammarTools: 'boolean',
60
- supportsToolSearch: 'boolean',
61
- supportsExplicitPromptCacheMode: 'boolean',
62
- }
63
-
64
- /** anthropic-messages 的 9 个字段(pi-ai AnthropicMessagesCompat)。 */
65
- const ANTHROPIC_FIELDS: Record<string, CompatValue> = {
66
141
  supportsEagerToolInputStreaming: 'boolean',
67
- supportsLongCacheRetention: 'boolean',
68
- sendSessionAffinityHeaders: 'boolean',
69
142
  supportsCacheControlOnTools: 'boolean',
70
143
  supportsTemperature: 'boolean',
71
144
  forceAdaptiveThinking: 'boolean',
72
145
  allowEmptySignature: 'boolean',
73
146
  supportsStrictTools: 'boolean',
74
- supportsToolReferences: 'boolean',
75
- }
76
-
77
- const FIELDS_BY_PROTOCOL: Record<ProtocolId, Record<string, CompatValue>> = {
78
- 'openai-completions': COMPLETIONS_FIELDS,
79
- 'openai-responses': RESPONSES_FIELDS,
80
- 'anthropic-messages': ANTHROPIC_FIELDS,
81
147
  }
82
148
 
83
- /** 某协议的全部合法 compat 键(UI 渲染字段组与校验共用)。 */
149
+ /** 某协议全部可配置(offer)的 compat 键(UI 渲染字段组与校验共用)。 */
84
150
  export function compatFieldsOf(api: ProtocolId): readonly string[] {
85
- return Object.keys(FIELDS_BY_PROTOCOL[api])
151
+ return Object.entries(GATES_BY_PROTOCOL[api]).flatMap(([field, disposition]) =>
152
+ disposition === 'offer' ? [field] : [],
153
+ )
86
154
  }
87
155
 
88
- /** 某协议某字段的取值约束(UI 渲染开关/下拉用)。 */
156
+ /** 某协议某 offer 字段的取值约束(UI 渲染开关/下拉用)。 */
89
157
  export function compatFieldSpec(api: ProtocolId, field: string): CompatValue | undefined {
90
- return FIELDS_BY_PROTOCOL[api][field]
158
+ return GATES_BY_PROTOCOL[api][field] === 'offer' ? VALUE_SPECS[field] : undefined
91
159
  }
92
160
 
93
- function checkValue(
94
- _api: ProtocolId,
95
- field: string,
96
- spec: CompatValue,
97
- value: unknown,
98
- where: string,
99
- ): void {
161
+ function checkValue(field: string, spec: CompatValue, value: unknown, where: string): void {
100
162
  if (spec === 'boolean') {
101
163
  if (typeof value !== 'boolean') throw new Error(`${where}: compat.${field} 必须是布尔值`)
102
164
  return
@@ -115,8 +177,11 @@ function checkValue(
115
177
  }
116
178
 
117
179
  /**
118
- * 校验一份 compat 字典对指定协议合法:未知协议/未知键拒绝(对比官方的静默丢弃),
119
- * 已知键校验值类型/枚举。undefined 值视为未设置,跳过(语义同 pi-ai 的 ??)。
180
+ * 校验一份 compat 字典对指定协议合法(对齐官方门控语义 + schema 值约束):
181
+ * - 未知键/withhold 字段拒绝(官方 0.1.2-alpha.1 写时拒绝,替代旧版静默丢弃);
182
+ * - 值类型/枚举按官方 schema 校验;
183
+ * - 无值键(null/undefined)拒绝(官方 assertOfferedCompatFields 同款:
184
+ * "写了但没生效" 的表面状态不允许)。
120
185
  */
121
186
  export function validateCompat(
122
187
  api: string,
@@ -124,26 +189,35 @@ export function validateCompat(
124
189
  where: string,
125
190
  ): void {
126
191
  if (compat === undefined) return
127
- const fields = FIELDS_BY_PROTOCOL[api as ProtocolId]
128
- if (fields === undefined) {
192
+ const gate = GATES_BY_PROTOCOL[api as ProtocolId]
193
+ if (gate === undefined) {
129
194
  throw new Error(
130
- `${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(支持:${Object.keys(FIELDS_BY_PROTOCOL).join(', ')})`,
195
+ `${where}: 协议 ${JSON.stringify(api)} 无 compat 字段表(支持:${Object.keys(GATES_BY_PROTOCOL).join(', ')})`,
131
196
  )
132
197
  }
198
+ const offered = compatFieldsOf(api as ProtocolId)
133
199
  for (const [key, value] of Object.entries(compat)) {
134
- const spec = fields[key]
135
- if (spec === undefined) {
200
+ const disposition = gate[key]
201
+ if (disposition !== 'offer') {
202
+ if (disposition === 'withhold') {
203
+ throw new Error(
204
+ `${where}: compat.${key} 官方按协议 withhold(内置目录已为对应厂商设置该开关);` +
205
+ '请以目录 provider 名作为 route 名(继承目录值),或移除该字段',
206
+ )
207
+ }
136
208
  throw new Error(
137
- `${where}: compat.${key} 不是 ${api} 协议的合法字段(合法字段:${Object.keys(fields).join(', ')})`,
209
+ `${where}: compat.${key} 不是 ${api} 协议的合法字段(可配置字段:${offered.join(', ')})`,
138
210
  )
139
211
  }
140
- if (value === undefined) continue
141
- checkValue(api, key, spec, value, where)
212
+ if (value === undefined || value === null) {
213
+ throw new Error(`${where}: compat.${key} 未设置值;给出值或移除该键(留空不会生效)`)
214
+ }
215
+ checkValue(key, VALUE_SPECS[key] as CompatValue, value, where)
142
216
  }
143
217
  }
144
218
 
145
219
  /**
146
- * 逐字段合并 compat 层(后者覆盖前者),丢弃 undefined 值。
220
+ * 逐字段合并 compat 层(后者覆盖前者),丢弃 undefined/null 值。
147
221
  * 层序:继承源(仅同协议)→ route 级 → 模型级。
148
222
  */
149
223
  export function mergeCompat(
@@ -153,7 +227,7 @@ export function mergeCompat(
153
227
  for (const layer of layers) {
154
228
  if (layer === undefined) continue
155
229
  for (const [key, value] of Object.entries(layer)) {
156
- if (value !== undefined) merged[key] = value
230
+ if (value !== undefined && value !== null) merged[key] = value
157
231
  }
158
232
  }
159
233
  return Object.keys(merged).length > 0 ? merged : undefined
package/src/config-api.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * 自定义端点 HTTP 通道:配置读写已迁移到官方 settings RPC(rc7 起第三方
3
- * 命名空间全量开放),这里只保留官方传输没有的「模型目录」端点(含
4
- * kitSource / models-dev 运行期诊断,供卡片状态行展示)。
2
+ * 自定义端点 HTTP 通道:配置读写已迁移到官方 remote.settings 直连
3
+ * (0.1.2-alpha.1 起第三方命名空间全量开放),这里只保留官方传输没有的
4
+ * 「模型目录」端点(含 kitSource / models-dev 运行期诊断,供卡片状态行展示)。
5
5
  * 仅监听 dsh web 同源(webserver 默认 127.0.0.1),与 GUI 其余面同等暴露面。
6
6
  * @module llm-pi/config-api
7
7
  */
package/src/config.ts CHANGED
@@ -37,14 +37,19 @@ export type AdapterKind = (typeof ADAPTER_KINDS)[number]
37
37
  /** deepseek 路由的思考/推理档位(线协议枚举,对齐官方 llm-deepseek)。 */
38
38
  export const DEEPSEEK_THINKING = ['enabled', 'disabled'] as const
39
39
  export const DEEPSEEK_REASONING_EFFORTS = ['off', 'low', 'high', 'max'] as const
40
+ /** 旧版 imageDetail 枚举(0.1.2-alpha.1 已移除该字段;仅用于 schema 透传陷阱的取值集)。 */
40
41
  export const DEEPSEEK_IMAGE_DETAILS = ['auto', 'low'] as const
41
42
 
42
43
  export const DEFAULT_CONTEXT_WINDOW = 262144
43
44
  export const DEFAULT_MAX_TOKENS = 32768
44
45
  export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000
45
- /** 单请求 base64 图片载荷上限(rc8 起官方 ResolvedPiAiProviderProfile 必需字段;
46
+ /** 单请求 base64 图片载荷上限(官方 ResolvedPiAiProviderProfile 必需字段;
46
47
  * 缺省取官方默认 20MiB,旧配置无需改动即自动生效)。 */
47
48
  export const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024
49
+ /** 单请求每个确定性内联版本的像素总预算(官方 DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET 同值)。 */
50
+ export const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048
51
+ /** 内联版本编码字节目标(官方 DEFAULT_REQUEST_IMAGE_MAX_BYTES 同值)。 */
52
+ export const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024
48
53
  /** dsh-timeout 的定时器上限(与官方 MAX_TIMER_DELAY_MS 对齐)。 */
49
54
  export const MAX_TIMER_DELAY_MS = 2 ** 31 - 1
50
55
 
@@ -68,7 +73,6 @@ export interface ModelEntryConfig {
68
73
  /** 以下仅 adapter: deepseek 的 route 有效(官方 llm-deepseek 目录字段)。 */
69
74
  imagePixelBudget?: number
70
75
  imageMaxBytes?: number
71
- imageDetail?: 'auto' | 'low'
72
76
  }
73
77
 
74
78
  /** 单个 provider route 配置(providers 字典的值)。 */
@@ -97,6 +101,8 @@ export interface ProviderProfileConfig {
97
101
  websocketConnectTimeoutMs?: number
98
102
  streamIdleTimeoutMs?: number
99
103
  maxRequestImageBytes?: number
104
+ requestImagePixelBudget?: number
105
+ requestImageMaxBytes?: number
100
106
  retryPolicy?: unknown
101
107
  models?: ModelEntryConfig[]
102
108
  /** 以下仅 adapter: deepseek 的 route 有效(透传官方 llm-deepseek 同名策略)。 */
@@ -167,9 +173,12 @@ const modelEntry = z.object({
167
173
  .step(1)
168
174
  .min(1)
169
175
  .description('(仅 adapter: deepseek)单张请求图片的编码字节上限;缺省继承官方目录'),
176
+ // 0.1.2-alpha.1 移除字段的 schema 透传陷阱:官方已删除 imageDetail(改用
177
+ // imagePixelBudget/imageMaxBytes),此处保留取值 union 让旧配置通过 schema、
178
+ // 在写时校验(assertServiceable)以中文迁移提示明确拒绝(见 profiles-deepseek.ts)。
170
179
  imageDetail: z
171
180
  .union(DEEPSEEK_IMAGE_DETAILS)
172
- .description('(仅 adapter: deepseek)图片细节档位;low 使用 512x512 像素预算'),
181
+ .description('(已移除)图片细节档位;0.1.2-alpha.1 起请改用 imagePixelBudget/imageMaxBytes'),
173
182
  })
174
183
 
175
184
  const providerProfile = z.object({
@@ -215,7 +224,19 @@ const providerProfile = z.object({
215
224
  .description('单支流式读取的最大空闲间隔毫秒'),
216
225
  maxRequestImageBytes: z
217
226
  .natural()
218
- .description('单请求 base64 图片载荷上限字节;缺省 20MiB(rc8 起生效,旧配置免改)'),
227
+ .description(
228
+ '单请求 base64 图片载荷上限字节;缺省 20MiB(0.1.2-alpha.1 起官方必需字段,旧配置免改)',
229
+ ),
230
+ requestImagePixelBudget: z
231
+ .number()
232
+ .step(1)
233
+ .min(1)
234
+ .description('单请求每个确定性内联图片版本的像素总预算;缺省 2048x2048(官方同值)'),
235
+ requestImageMaxBytes: z
236
+ .number()
237
+ .step(1)
238
+ .min(1)
239
+ .description('内联图片版本编码字节目标;缺省 1MiB(官方同值)'),
219
240
  retryPolicy: z.any().description('provider 重试策略(dsh-llm RetryPolicy 形状,构建期校验)'),
220
241
  thinking: z
221
242
  .union(DEEPSEEK_THINKING)
@@ -10,6 +10,9 @@
10
10
  * 热更新:适配器 options thunk 每次操作重读当前物化产物(连接事实变化下一
11
11
  * 请求生效);retryPolicy 是注册期捕获事实,变化时 handle.replace 原地重注册
12
12
  * (官方同款模式);route 移除即 dispose 释放路由名。
13
+ *
14
+ * 0.1.2-alpha.1:DeepSeekAdapterOptions 新增必需 prepareExtensions(官方接线
15
+ * ctx.get('deepseekLlmApiExtensions'),缺省空实现)与可选 resolveImageAccess。
13
16
  * @module llm-pi/deepseek-routes
14
17
  */
15
18
  import type { Context } from '@deepseek-ai/cordis'
@@ -130,6 +133,28 @@ export class DeepseekRouteRegistrar {
130
133
  resolveApiKey: this.resolveApiKey as never,
131
134
  resolveUserId: () => this.resolveUserId() as never,
132
135
  resolveAttachments: () => ctx.get('attachments') as never,
136
+ // 附件宿主路径 → 模型工具执行世界只读路径桥(官方接线同款,见
137
+ // llm-deepseek/src/index.ts:447-452)。
138
+ resolveImageAccess: (attachments, ref) =>
139
+ kit.resolveImageAttachmentAccess(
140
+ attachments,
141
+ (hostPath) =>
142
+ (
143
+ ctx.get('fs') as
144
+ | { processPathFromHostPath(hostPath: string): string | undefined }
145
+ | undefined
146
+ )?.processPathFromHostPath(hostPath),
147
+ ref,
148
+ ) as never,
149
+ // 0.1.2-alpha.1 必需:官方扩展注册表(deepseekLlmApiExtensions)缺席时
150
+ // 给空实现(官方接线范式,llm-deepseek/src/index.ts:404-470)。
151
+ prepareExtensions: (request) => {
152
+ const extensions = ctx.get('deepseekLlmApiExtensions') as
153
+ | { prepare(request: unknown): Promise<unknown> }
154
+ | undefined
155
+ return (extensions?.prepare(request) ??
156
+ Promise.resolve({ fields: {}, accept: () => Promise.resolve() })) as never
157
+ },
133
158
  })
134
159
  // 官方 providerInfo 硬编码 name: "DeepSeek"——覆盖为按路由动态读取的
135
160
  // displayName,否则自定义 deepseek 路由在模型选择器里与 deepseek-official
package/src/ns.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
3
3
  * 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
4
4
  * settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
5
- * 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
5
+ * 契约:0.1.2-alpha.1 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
6
6
  * 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
7
7
  * @module llm-pi/ns
8
8
  */
@@ -130,19 +130,25 @@ function materializeModel(
130
130
  if (entry.reasoningEfforts !== undefined || nonEmptyDict(entry.compat)) {
131
131
  invalid(route, `model "${entry.id}" 的 reasoningEfforts/compat 仅 adapter: pi 可用`)
132
132
  }
133
+ const legacyImageDetail = (entry as ModelEntryConfig & { imageDetail?: unknown }).imageDetail
134
+ if (legacyImageDetail !== undefined) {
135
+ // 0.1.2-alpha.1 移除字段:写时明确拒绝并给出迁移提示(官方 llm-deepseek
136
+ // 对含 imageDetail 的目录模型同样直接抛错,index.ts:212-215)。
137
+ invalid(
138
+ route,
139
+ `model "${entry.id}" 的 imageDetail 已随 0.1.2-alpha.1 移除;` +
140
+ '请改用 imagePixelBudget/imageMaxBytes(低细节可配更小的 imagePixelBudget)并删除 imageDetail',
141
+ )
142
+ }
133
143
  const base = resolveOfficialBase(route, entry, deps)
134
144
  const input = declaredModalities(entry.input) ?? base.input
135
145
  const imageFields = {
136
146
  imagePixelBudget: entry.imagePixelBudget ?? base.imagePixelBudget,
137
147
  imageMaxBytes: entry.imageMaxBytes ?? base.imageMaxBytes,
138
- imageDetail: entry.imageDetail ?? base.imageDetail,
139
148
  }
140
149
  const acceptsImage = input?.includes('image') ?? false
141
150
  if (!acceptsImage && Object.values(imageFields).some((value) => value !== undefined)) {
142
- invalid(
143
- route,
144
- `model "${entry.id}" 未声明 image 模态,不可配置 imagePixelBudget/imageMaxBytes/imageDetail`,
145
- )
151
+ invalid(route, `model "${entry.id}" 未声明 image 模态,不可配置 imagePixelBudget/imageMaxBytes`)
146
152
  }
147
153
  return {
148
154
  id: entry.id,
@@ -227,7 +233,7 @@ function buildRoute(
227
233
  if (deps.kit.deepseek === undefined) {
228
234
  invalid(
229
235
  route,
230
- '使用 adapter: deepseek,但当前运行时套件不含 dsh-llm-deepseek(需 dsh ≥ 0.1.1-rc.2)',
236
+ '使用 adapter: deepseek,但当前运行时套件不含 dsh-llm-deepseek0.1.2-alpha.1 基线)',
231
237
  )
232
238
  }
233
239
  rejectPiOnlyFields(route, profile)