@morlay/dsh-llm-openai-compatible 0.0.9-alpha.8 → 0.0.9-alpha.9

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
@@ -6,6 +6,12 @@ DeepSeek Harness 的 **OpenAI 兼容 LLM 适配器**插件。与内置 `llm-pi-a
6
6
  `seed`),请求级 `GenerateOptions.temperature` 优先于 profile 默认值;`providers`
7
7
  用 dict 多路由结构(与 `llm-pi-ai` 一致)。
8
8
 
9
+ ## 可选:与官方 `llm-pi-ai` 二选一
10
+
11
+ 本包是**可选 bundle**,与官方 `llm-pi-ai` 覆盖同一块领域:两者都提供 openai-compatible 的 `providers` dict 与
12
+ 同名的 provider 路由。**同装会出现两套适配器抢同一批路由**,所以一个部署只装一个——本部署装的是官方那行
13
+ (`@morlay/dsh-profile` 配它的 `ollama` route);只有需要 profile 级采样默认值时才换成这一行。
14
+
9
15
  传输层复用 **[@ai-sdk/openai-compatible](https://www.npmjs.com/package/@ai-sdk/openai-compatible)**
10
16
  (wire 序列化与 SSE 解析由 SDK 负责);本插件负责 harness 消息 → AI SDK prompt
11
17
  转换、采样默认合并、stream part → `StreamChunk` 翻译、错误归一化与凭据策略。
package/dist/index.mjs CHANGED
@@ -259,34 +259,114 @@ const REASONING_LEVELS = [
259
259
  "max"
260
260
  ];
261
261
  const MODEL_MODALITIES = ["text", "image"];
262
+ /**
263
+ * 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
264
+ * (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
265
+ */
266
+ const localized = (text) => text;
262
267
  const modelSchema = z.object({
263
- id: z.string().required(),
264
- name: z.string(),
265
- description: z.string(),
266
- contextWindow: z.number().step(1).min(1),
267
- maxTokens: z.number().step(1).min(1),
268
- inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(["text"]),
269
- reasoningEfforts: z.union([z.const(false), z.dict(z.union([z.string(), z.const(null)]))])
268
+ id: z.string().required().description(localized({
269
+ zh: "模型 id:请求里发出去的那个名字。",
270
+ en: "Model id: the name sent in requests."
271
+ })),
272
+ name: z.string().description(localized({
273
+ zh: "显示名;省略就用 id。",
274
+ en: "Display name; the id is used when omitted."
275
+ })),
276
+ description: z.string().description(localized({
277
+ zh: "模型选择器里的一句话说明。",
278
+ en: "One-line description shown in model pickers."
279
+ })),
280
+ contextWindow: z.number().step(1).min(1).description(localized({
281
+ zh: "上下文窗口(token 数);省略就用服务商的 defaultContextWindow。",
282
+ en: "Context window in tokens; unset falls back to the provider's defaultContextWindow."
283
+ })),
284
+ maxTokens: z.number().step(1).min(1).description(localized({
285
+ zh: "单次回复的输出上限;省略就用服务商的 defaultMaxTokens。",
286
+ en: "Output cap per reply; unset falls back to the provider's defaultMaxTokens."
287
+ })),
288
+ inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(["text"]).description(localized({
289
+ zh: "这个模型接受的输入模态。",
290
+ en: "Input modalities this model accepts."
291
+ })),
292
+ reasoningEfforts: z.union([z.const(false), z.dict(z.union([z.string(), z.const(null)]))]).description(localized({
293
+ zh: "思考档位:`false` 表示不支持;给一份档位表(档位名 → 可选的模型侧取值)表示支持。",
294
+ en: "Reasoning efforts: `false` when unsupported; a level table (level name to optional model-side value) when supported."
295
+ }))
270
296
  });
271
297
  const providerSchema = z.object({
272
- apiKeyEnv: z.string().role("credential-ref"),
273
- displayName: z.string(),
274
- baseURL: z.string().required(),
275
- headers: z.dict(z.string()),
276
- temperature: z.number().min(0).max(2),
277
- topP: z.number().min(0).max(1),
278
- topK: z.number().step(1).min(1),
279
- presencePenalty: z.number().min(-2).max(2),
280
- frequencyPenalty: z.number().min(-2).max(2),
281
- seed: z.number().step(1).min(1),
282
- reasoning: z.union(REASONING_LEVELS),
283
- models: z.array(modelSchema),
284
- defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
285
- defaultMaxTokens: z.number().step(1).min(1).default(DEFAULT_MAX_TOKENS),
286
- maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
287
- streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
288
- timeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS),
289
- retryPolicy: RetryPolicySchema
298
+ apiKeyEnv: z.string().role("credential-ref").description(localized({
299
+ zh: "凭证引用名:每次请求经凭证服务解析一次,别把密钥写在配置里。",
300
+ en: "Credential reference resolved through the credentials service per request; never inline secrets."
301
+ })),
302
+ displayName: z.string().description(localized({
303
+ zh: "显示名;省略就用 provider 的键名。",
304
+ en: "Display name; the provider key is used when omitted."
305
+ })),
306
+ baseURL: z.string().required().description(localized({
307
+ zh: "OpenAI 兼容端点根(不含 `/chat/completions`)。",
308
+ en: "OpenAI-compatible endpoint root (without `/chat/completions`)."
309
+ })),
310
+ headers: z.dict(z.string()).description(localized({
311
+ zh: "额外请求头,逐项发给端点。",
312
+ en: "Extra request headers sent to the endpoint."
313
+ })),
314
+ temperature: z.number().min(0).max(2).description(localized({
315
+ zh: "采样温度(0–2)。",
316
+ en: "Sampling temperature (0-2)."
317
+ })),
318
+ topP: z.number().min(0).max(1).description(localized({
319
+ zh: "核采样(0–1)。",
320
+ en: "Nucleus sampling (0-1)."
321
+ })),
322
+ topK: z.number().step(1).min(1).description(localized({
323
+ zh: "top-k 采样。",
324
+ en: "top-k sampling."
325
+ })),
326
+ presencePenalty: z.number().min(-2).max(2).description(localized({
327
+ zh: "存在惩罚(−2–2)。",
328
+ en: "Presence penalty (-2 to 2)."
329
+ })),
330
+ frequencyPenalty: z.number().min(-2).max(2).description(localized({
331
+ zh: "频率惩罚(−2–2)。",
332
+ en: "Frequency penalty (-2 to 2)."
333
+ })),
334
+ seed: z.number().step(1).min(1).description(localized({
335
+ zh: "随机种子:固定它让采样尽量可复现(端点支持时)。",
336
+ en: "Random seed: pins sampling where the endpoint supports it."
337
+ })),
338
+ reasoning: z.union(REASONING_LEVELS).description(localized({
339
+ zh: "思考档位:这一端支持哪些(与模型自己的档位表对齐)。",
340
+ en: "Which reasoning levels this endpoint accepts (paired with each model's own table)."
341
+ })),
342
+ models: z.array(modelSchema).description(localized({
343
+ zh: "这个端点提供的模型;每项一个模型。",
344
+ en: "Models this endpoint serves, one entry each."
345
+ })),
346
+ defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW).description(localized({
347
+ zh: "没写 contextWindow 的模型用这个上下文窗口。",
348
+ en: "Context window used by models that declare none."
349
+ })),
350
+ defaultMaxTokens: z.number().step(1).min(1).default(DEFAULT_MAX_TOKENS).description(localized({
351
+ zh: "没写 maxTokens 的模型用这个输出上限。",
352
+ en: "Output cap used by models that declare none."
353
+ })),
354
+ maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES).description(localized({
355
+ zh: "单次请求里图片的总字节上限;超过的图片会被拒绝。",
356
+ en: "Total image bytes allowed per request; larger payloads are refused."
357
+ })),
358
+ streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS).description(localized({
359
+ zh: "流式响应两次数据之间的最长空闲(毫秒):超时按可重试的断流处理。",
360
+ en: "Longest idle gap between stream chunks in ms; a longer gap counts as a retryable drop."
361
+ })),
362
+ timeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).description(localized({
363
+ zh: "单次请求的总超时(毫秒);省略就只受流式空闲限制。",
364
+ en: "Overall per-request timeout in ms; unset leaves only the stream idle guard."
365
+ })),
366
+ retryPolicy: RetryPolicySchema.description(localized({
367
+ zh: "重试策略:哪些失败重试、退避与上限。",
368
+ en: "Retry policy: which failures retry, backoff, and caps."
369
+ }))
290
370
  });
291
371
  const Config = z.object({ providers: z.dict(providerSchema).default({}).volatile() });
292
372
  function isReasoningEffort(value) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morlay/dsh-llm-openai-compatible",
3
- "version": "0.0.9-alpha.8",
4
- "description": "OpenAI-compatible LLM adapter plugin for DeepSeek Harness with configurable default sampling parameters (temperature / topP / topK / penalties / seed) over a providers dict.",
3
+ "version": "0.0.9-alpha.9",
4
+ "description": "Optional OpenAI-compatible LLM adapter plugin for DeepSeek Harness with configurable default sampling parameters (temperature / topP / topK / penalties / seed) over a providers dict. Pick either this bundle or the built-in llm-pi-ai, never both.",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
@@ -19,39 +19,32 @@
19
19
  "locale/*.json",
20
20
  "dist",
21
21
  "src",
22
- "cordis.patch.yml",
23
22
  "!**/__tests__"
24
23
  ],
25
24
  "type": "module",
26
25
  "exports": {
27
26
  ".": "./dist/index.mjs",
28
- "./invariant": "./dist/invariant.mjs",
29
27
  "./wire": "./dist/wire.mjs",
28
+ "./invariant": "./dist/invariant.mjs",
30
29
  "./package.json": "./package.json",
31
- "./cordis.patch.yml": "./cordis.patch.yml",
32
30
  "./locale/*.json": "./locale/*.json"
33
31
  },
34
32
  "dependencies": {
35
33
  "@ai-sdk/openai-compatible": "^3.0.32",
36
34
  "@ai-sdk/provider": "^4.0.7",
37
- "@deepseek-ai/dsh-util-values": "0.1.7-rc.1"
35
+ "@deepseek-ai/dsh-util-values": "^0.1.7-rc.2"
38
36
  },
39
37
  "peerDependencies": {
40
- "@deepseek-ai/cordis": "4.0.4",
41
- "@deepseek-ai/dsh-anonymous-user-id": "0.1.7-rc.1",
42
- "@deepseek-ai/dsh-attachment": "0.1.7-rc.1",
43
- "@deepseek-ai/dsh-credentials": "0.1.7-rc.1",
44
- "@deepseek-ai/dsh-invariants": "0.1.7-rc.1",
45
- "@deepseek-ai/dsh-launch-environment": "0.1.7-rc.1",
46
- "@deepseek-ai/dsh-llm": "0.1.7-rc.1",
47
- "@deepseek-ai/dsh-settings": "0.1.7-rc.1",
48
- "@deepseek-ai/dsh-timeout": "0.1.7-rc.1",
49
- "@deepseek-ai/schemastery": "3.18.4"
50
- },
51
- "dsh": {
52
- "bundle": {
53
- "patch": "./cordis.patch.yml"
54
- }
38
+ "@deepseek-ai/cordis": "^4.0.4",
39
+ "@deepseek-ai/dsh-anonymous-user-id": "^0.1.7-rc.2",
40
+ "@deepseek-ai/dsh-attachment": "^0.1.7-rc.2",
41
+ "@deepseek-ai/dsh-credentials": "^0.1.7-rc.2",
42
+ "@deepseek-ai/dsh-invariants": "^0.1.7-rc.2",
43
+ "@deepseek-ai/dsh-launch-environment": "^0.1.7-rc.2",
44
+ "@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
45
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.2",
46
+ "@deepseek-ai/dsh-timeout": "^0.1.7-rc.2",
47
+ "@deepseek-ai/schemastery": "^3.18.4"
55
48
  },
56
49
  "scripts": {
57
50
  "build": "pnpm exec tsdown"
package/src/index.ts CHANGED
@@ -93,39 +93,237 @@ export interface Config {
93
93
  /** 解析成普通值之后的配置形状(校验与解析只认它)。 */
94
94
  export type Options = { [K in keyof Config]?: Config[K] extends Volatile<infer T> ? T : never };
95
95
 
96
+ /**
97
+ * 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
98
+ * (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
99
+ */
100
+ const localized = (text: { zh: string; en: string }): string => text as unknown as string;
101
+
96
102
  const modelSchema = z.object({
97
- id: z.string().required(),
98
- name: z.string(),
99
- description: z.string(),
100
- contextWindow: z.number().step(1).min(1),
101
- maxTokens: z.number().step(1).min(1),
102
- inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(["text"]),
103
- reasoningEfforts: z.union([z.const(false), z.dict(z.union([z.string(), z.const(null)]))]),
103
+ id: z
104
+ .string()
105
+ .required()
106
+ .description(
107
+ localized({
108
+ zh: "模型 id:请求里发出去的那个名字。",
109
+ en: "Model id: the name sent in requests.",
110
+ }),
111
+ ),
112
+ name: z.string().description(
113
+ localized({
114
+ zh: "显示名;省略就用 id。",
115
+ en: "Display name; the id is used when omitted.",
116
+ }),
117
+ ),
118
+ description: z.string().description(
119
+ localized({
120
+ zh: "模型选择器里的一句话说明。",
121
+ en: "One-line description shown in model pickers.",
122
+ }),
123
+ ),
124
+ contextWindow: z
125
+ .number()
126
+ .step(1)
127
+ .min(1)
128
+ .description(
129
+ localized({
130
+ zh: "上下文窗口(token 数);省略就用服务商的 defaultContextWindow。",
131
+ en: "Context window in tokens; unset falls back to the provider's defaultContextWindow.",
132
+ }),
133
+ ),
134
+ maxTokens: z
135
+ .number()
136
+ .step(1)
137
+ .min(1)
138
+ .description(
139
+ localized({
140
+ zh: "单次回复的输出上限;省略就用服务商的 defaultMaxTokens。",
141
+ en: "Output cap per reply; unset falls back to the provider's defaultMaxTokens.",
142
+ }),
143
+ ),
144
+ inputModalities: z
145
+ .array(z.union(MODEL_MODALITIES))
146
+ .min(1)
147
+ .default(["text"])
148
+ .description(
149
+ localized({
150
+ zh: "这个模型接受的输入模态。",
151
+ en: "Input modalities this model accepts.",
152
+ }),
153
+ ),
154
+ reasoningEfforts: z
155
+ .union([z.const(false), z.dict(z.union([z.string(), z.const(null)]))])
156
+ .description(
157
+ localized({
158
+ zh: "思考档位:`false` 表示不支持;给一份档位表(档位名 → 可选的模型侧取值)表示支持。",
159
+ en: "Reasoning efforts: `false` when unsupported; a level table (level name to optional model-side value) when supported.",
160
+ }),
161
+ ),
104
162
  });
105
163
 
106
164
  const providerSchema: z<ProviderProfileSource> = z.object({
107
- apiKeyEnv: z.string().role("credential-ref"),
108
- displayName: z.string(),
109
- baseURL: z.string().required(),
110
- headers: z.dict(z.string()),
111
- temperature: z.number().min(0).max(2),
112
- topP: z.number().min(0).max(1),
113
- topK: z.number().step(1).min(1),
114
- presencePenalty: z.number().min(-2).max(2),
115
- frequencyPenalty: z.number().min(-2).max(2),
116
- seed: z.number().step(1).min(1),
117
- reasoning: z.union(REASONING_LEVELS),
118
- models: z.array(modelSchema),
119
- defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
120
- defaultMaxTokens: z.number().step(1).min(1).default(DEFAULT_MAX_TOKENS),
121
- maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
165
+ apiKeyEnv: z
166
+ .string()
167
+ .role("credential-ref")
168
+ .description(
169
+ localized({
170
+ zh: "凭证引用名:每次请求经凭证服务解析一次,别把密钥写在配置里。",
171
+ en: "Credential reference resolved through the credentials service per request; never inline secrets.",
172
+ }),
173
+ ),
174
+ displayName: z.string().description(
175
+ localized({
176
+ zh: "显示名;省略就用 provider 的键名。",
177
+ en: "Display name; the provider key is used when omitted.",
178
+ }),
179
+ ),
180
+ baseURL: z
181
+ .string()
182
+ .required()
183
+ .description(
184
+ localized({
185
+ zh: "OpenAI 兼容端点根(不含 `/chat/completions`)。",
186
+ en: "OpenAI-compatible endpoint root (without `/chat/completions`).",
187
+ }),
188
+ ),
189
+ headers: z.dict(z.string()).description(
190
+ localized({
191
+ zh: "额外请求头,逐项发给端点。",
192
+ en: "Extra request headers sent to the endpoint.",
193
+ }),
194
+ ),
195
+ temperature: z
196
+ .number()
197
+ .min(0)
198
+ .max(2)
199
+ .description(
200
+ localized({
201
+ zh: "采样温度(0–2)。",
202
+ en: "Sampling temperature (0-2).",
203
+ }),
204
+ ),
205
+ topP: z
206
+ .number()
207
+ .min(0)
208
+ .max(1)
209
+ .description(
210
+ localized({
211
+ zh: "核采样(0–1)。",
212
+ en: "Nucleus sampling (0-1).",
213
+ }),
214
+ ),
215
+ topK: z
216
+ .number()
217
+ .step(1)
218
+ .min(1)
219
+ .description(
220
+ localized({
221
+ zh: "top-k 采样。",
222
+ en: "top-k sampling.",
223
+ }),
224
+ ),
225
+ presencePenalty: z
226
+ .number()
227
+ .min(-2)
228
+ .max(2)
229
+ .description(
230
+ localized({
231
+ zh: "存在惩罚(−2–2)。",
232
+ en: "Presence penalty (-2 to 2).",
233
+ }),
234
+ ),
235
+ frequencyPenalty: z
236
+ .number()
237
+ .min(-2)
238
+ .max(2)
239
+ .description(
240
+ localized({
241
+ zh: "频率惩罚(−2–2)。",
242
+ en: "Frequency penalty (-2 to 2).",
243
+ }),
244
+ ),
245
+ seed: z
246
+ .number()
247
+ .step(1)
248
+ .min(1)
249
+ .description(
250
+ localized({
251
+ zh: "随机种子:固定它让采样尽量可复现(端点支持时)。",
252
+ en: "Random seed: pins sampling where the endpoint supports it.",
253
+ }),
254
+ ),
255
+ reasoning: z.union(REASONING_LEVELS).description(
256
+ localized({
257
+ zh: "思考档位:这一端支持哪些(与模型自己的档位表对齐)。",
258
+ en: "Which reasoning levels this endpoint accepts (paired with each model's own table).",
259
+ }),
260
+ ),
261
+ models: z.array(modelSchema).description(
262
+ localized({
263
+ zh: "这个端点提供的模型;每项一个模型。",
264
+ en: "Models this endpoint serves, one entry each.",
265
+ }),
266
+ ),
267
+ defaultContextWindow: z
268
+ .number()
269
+ .step(1)
270
+ .min(1)
271
+ .default(DEFAULT_CONTEXT_WINDOW)
272
+ .description(
273
+ localized({
274
+ zh: "没写 contextWindow 的模型用这个上下文窗口。",
275
+ en: "Context window used by models that declare none.",
276
+ }),
277
+ ),
278
+ defaultMaxTokens: z
279
+ .number()
280
+ .step(1)
281
+ .min(1)
282
+ .default(DEFAULT_MAX_TOKENS)
283
+ .description(
284
+ localized({
285
+ zh: "没写 maxTokens 的模型用这个输出上限。",
286
+ en: "Output cap used by models that declare none.",
287
+ }),
288
+ ),
289
+ maxRequestImageBytes: z
290
+ .number()
291
+ .step(1)
292
+ .min(1)
293
+ .default(DEFAULT_MAX_REQUEST_IMAGE_BYTES)
294
+ .description(
295
+ localized({
296
+ zh: "单次请求里图片的总字节上限;超过的图片会被拒绝。",
297
+ en: "Total image bytes allowed per request; larger payloads are refused.",
298
+ }),
299
+ ),
122
300
  streamIdleTimeoutMs: z
123
301
  .number()
124
302
  .min(Number.MIN_VALUE)
125
303
  .max(MAX_TIMER_DELAY_MS)
126
- .default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
127
- timeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS),
128
- retryPolicy: RetryPolicySchema,
304
+ .default(DEFAULT_STREAM_IDLE_TIMEOUT_MS)
305
+ .description(
306
+ localized({
307
+ zh: "流式响应两次数据之间的最长空闲(毫秒):超时按可重试的断流处理。",
308
+ en: "Longest idle gap between stream chunks in ms; a longer gap counts as a retryable drop.",
309
+ }),
310
+ ),
311
+ timeoutMs: z
312
+ .number()
313
+ .min(Number.MIN_VALUE)
314
+ .max(MAX_TIMER_DELAY_MS)
315
+ .description(
316
+ localized({
317
+ zh: "单次请求的总超时(毫秒);省略就只受流式空闲限制。",
318
+ en: "Overall per-request timeout in ms; unset leaves only the stream idle guard.",
319
+ }),
320
+ ),
321
+ retryPolicy: RetryPolicySchema.description(
322
+ localized({
323
+ zh: "重试策略:哪些失败重试、退避与上限。",
324
+ en: "Retry policy: which failures retry, backoff, and caps.",
325
+ }),
326
+ ),
129
327
  });
130
328
 
131
329
  export const Config = z.object({
package/cordis.patch.yml DELETED
@@ -1,3 +0,0 @@
1
- - insert:
2
- - id: llm-openai-compatible
3
- name: "@morlay/dsh-llm-openai-compatible"