@morlay/dsh-llm-openai-compatible 0.0.10 → 0.0.12

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,31 +1,20 @@
1
1
  # @morlay/dsh-llm-openai-compatible
2
2
 
3
- DeepSeek Harness 的 **OpenAI 兼容 LLM 适配器**插件。与内置 `llm-pi-ai` /
4
- `llm-deepseek` 不同,本插件的配置 schema 支持 **profile 级默认采样参数**
5
- (`temperature` / `topP` / `topK` / `presencePenalty` / `frequencyPenalty` /
6
- `seed`),请求级 `GenerateOptions.temperature` 优先于 profile 默认值;`providers`
7
- 用 dict 多路由结构(与 `llm-pi-ai` 一致)。
3
+ DeepSeek Harness 的 **OpenAI 兼容 LLM 适配器**:`providers` 是 dict 多路由(**key 就是 provider 路由键**),每个
4
+ profile 除了端点与模型目录,还能声明 **profile 级默认采样参数**(`temperature` / `topP` / `topK` /
5
+ `presencePenalty` / `frequencyPenalty` / `seed`)——请求级 `GenerateOptions.temperature` 优先于 profile 默认,缺省
6
+ 一律不发送(= 提供方默认)。传输层复用
7
+ **[@ai-sdk/openai-compatible](https://www.npmjs.com/package/@ai-sdk/openai-compatible)**,本插件只做 harness
8
+ 适配:消息 → AI SDK prompt、采样合并、stream part → `StreamChunk`、错误归一化与凭据策略。
8
9
 
9
- ## 可选:与官方 `llm-pi-ai` 二选一
10
+ **与官方 `llm-pi-ai` 二选一**:两者都提供 openai-compatible 的 `providers` dict 与同名 provider 路由,同装会有
11
+ 两套适配器抢同一批路由,一个部署只装一个。本部署装的是官方那行——它的 `ollama` route 由
12
+ [`@morlay/ollama-provider-profile`](../../bundles/ollama-provider-profile/cordis.patch.yml) 配置;只有需要 profile
13
+ 级采样默认值时才换成这一行。
10
14
 
11
- 本包是**可选 bundle**,与官方 `llm-pi-ai` 覆盖同一块领域:两者都提供 openai-compatible 的 `providers` dict 与
12
- 同名的 provider 路由。**同装会出现两套适配器抢同一批路由**,所以一个部署只装一个——本部署装的是官方那行
13
- (`@morlay/dsh-profile` 配它的 `ollama` route);只有需要 profile 级采样默认值时才换成这一行。
15
+ ## 用法
14
16
 
15
- 传输层复用 **[@ai-sdk/openai-compatible](https://www.npmjs.com/package/@ai-sdk/openai-compatible)**
16
- (wire 序列化与 SSE 解析由 SDK 负责);本插件负责 harness 消息 → AI SDK prompt
17
- 转换、采样默认合并、stream part → `StreamChunk` 翻译、错误归一化与凭据策略。
18
-
19
- 本文件只给配置面与用法;每条规则与取舍的 home 是各自 ADR(见下)。
20
-
21
- ## 配置
22
-
23
- `providers` 是 dict:**key 就是 provider 路由键**(选择器与
24
- `GenerateOptions.provider` 使用),值是 profile。它标了 `.volatile()`——**运行期可改**:
25
- 值经 Loader 的引用读取,设置页(Models 页)改的就是它,不需要重挂这行插件(见
26
- [ADR-运行期改配置走volatile引用](./.agents/adrs/20260922-运行期改配置走volatile引用.md))。
27
-
28
- 配置写在**这行的 config** 里(bundle patch / profile patch / 设置页都落到这里):
17
+ 配置写在这一行的 config 里(bundle patch / profile patch / 设置页都落到这里):
29
18
 
30
19
  ```yaml
31
20
  - id: llm-openai-compatible
@@ -36,64 +25,18 @@ DeepSeek Harness 的 **OpenAI 兼容 LLM 适配器**插件。与内置 `llm-pi-a
36
25
  apiKeyEnv: OLLAMA_API_KEY
37
26
  baseURL: https://ollama.com/v1
38
27
  displayName: Ollama Gateway
39
- # === 采样默认参数(请求级 temperature 优先)===
40
- temperature: 1 # 0..2
41
- topP: 0.95 # 0..1 → wire top_p
42
- topK: 40 # 正整数 → wire top_k(非标准,仅网关支持时发送)
43
- presencePenalty: 0 # -2..2 → wire presence_penalty
44
- frequencyPenalty: 0 # -2..2 → wire frequency_penalty
45
- seed: 42 # 正整数 → wire seed
46
- # === 推理 ===
47
- reasoning: high # 部署默认档位(省略 = 提供方默认)
48
- # === 模型目录 ===
49
- defaultContextWindow: 262144
50
- defaultMaxTokens: 32768
28
+ temperature: 1
29
+ topP: 0.95
30
+ reasoning: high
51
31
  models:
52
32
  - id: deepseek-v4-flash:0731
53
33
  name: DeepSeek V4 Flash
54
34
  contextWindow: 1000000
55
35
  maxTokens: 65535
56
36
  inputModalities: [text, image]
57
- reasoningEfforts:
58
- off: # off 空值 = 不发送 reasoning_effort
59
- high: high # 档位 → wire reasoning_effort 拼写
60
- max: max
61
- # === 传输 ===
62
- maxRequestImageBytes: 20971520
63
- streamIdleTimeoutMs: 300000
64
- timeoutMs: 600000 # 整体请求超时;缺省不设
65
- retryPolicy:
66
- mode: normal
67
- maxRetries: 5
37
+ reasoningEfforts: { off: null, high: high, max: max }
68
38
  ```
69
39
 
70
- > 旧 `$DSH_HOME/settings.yaml` 的 `llm-openai-compatible` 段由上游启动时一次性导入到同 id 的行 config。
71
-
72
- ## 规则与取舍
73
-
74
- 规则细节(合并表、wire 字段落点、端点与错误映射)在各自 ADR 里维护,这里只列结论:
75
-
76
- - **采样默认值与省略语义**:请求级优先于 profile 默认,缺省一律**不发送**(= 提供方
77
- 默认)——见 [ADR-采样默认值合并规则与省略语义](./.agents/adrs/20260917-采样默认值合并规则与省略语义.md)。
78
- - **模型目录缺省为空、描述模型绝不抛错**:未列出的 id 原样透传,不支持的能力配置推迟到
79
- 请求执行处失败——见 [ADR-模型目录缺省为空且描述模型绝不抛错](./.agents/adrs/20260917-模型目录缺省为空且描述模型绝不抛错.md)。
80
- - **传输层复用 SDK**:非标准字段(`top_k`)与用量方言在本层显式处理——见
81
- [ADR-传输层复用ai-sdk-openai-compatible而非自研wire序列化](./.agents/adrs/20260917-传输层复用ai-sdk-openai-compatible而非自研wire序列化.md)。
82
- - **凭据经 `ctx.credentials` 解析**(服务缺失回退 launch environment):见
83
- [ADR-凭据经credentials服务解析而非直接读环境变量](./.agents/adrs/20260917-凭据经credentials服务解析而非直接读环境变量.md)。
84
- - **图片超预算报错交由 durable offload 重试**(本适配器不自行裁剪请求图片):见
85
- [ADR-图片超预算报错交由durable-offload重试](./.agents/adrs/20260917-图片超预算报错交由durable-offload重试.md)。
86
- - **多路由结构对齐 `llm-pi-ai`**(数组式 profiles 被拒绝):见
87
- [ADR-providers采用dict多路由结构对齐llm-pi-ai](./.agents/adrs/20260917-providers采用dict多路由结构对齐llm-pi-ai.md)
88
- 与 [ADR-起因llm-pi-ai的请求参数配置不完整](./.agents/adrs/20260917-起因llm-pi-ai的请求参数配置不完整.md)。
89
- - **未做(YAGNI)**:`modelOverrides`、模型 discovery(`GET /models`)、OAuth / 非
90
- bearer 认证——理由见模型目录 ADR。
91
-
92
- ## 从 `llm-pi-ai` 迁移
93
-
94
- 把 `llm-pi-ai.providers.<route>` 的 `baseURL` / `models` / 采样字段平移到
95
- `llm-openai-compatible.providers.<route>`(无 `api` 字段——协议固定
96
- chat-completions),`apiKeyEnv` 与 `retryPolicy` 原样保留;`reasoningEfforts`
97
- 的 `off` 空值语义一致。
98
-
99
- 构建、测试与类型检查的命令见根 `justfile` 与 `mise.toml`。
40
+ `providers` 标了 `.volatile()`——**运行期可改**:值经 Loader 的引用读取,设置页(Models 页)改的就是它,不需要
41
+ 重挂这行插件。字段清单(端点与请求头、采样、模型目录、传输与重试)、wire 落点与省略语义、错误映射、凭据与图片
42
+ 策略都在各自 ADR 里;`$DSH_HOME/settings.yaml` 里同名的段由上游在启动时导入同 id 的行 config。
package/dist/index.d.mts CHANGED
@@ -7,12 +7,12 @@ import { Context, Volatile } from "@deepseek-ai/cordis";
7
7
  type Dict<T = any, K extends string | symbol = string> = { [key in K]: T; };
8
8
  //#endregion
9
9
  //#region src/index.d.ts
10
- declare const name = "llm-openai-compatible";
11
- declare const inject: string[];
12
- declare const NS = "llm-openai-compatible";
13
- declare const REASONING_LEVELS: readonly ["off", "low", "high", "max"];
14
- declare const MODEL_MODALITIES: readonly ["text", "image"];
15
- interface ModelProfileSource {
10
+ export declare const name = "llm-openai-compatible";
11
+ export declare const inject: string[];
12
+ export declare const NS = "llm-openai-compatible";
13
+ export declare const REASONING_LEVELS: readonly ["off", "low", "high", "max"];
14
+ export declare const MODEL_MODALITIES: readonly ["text", "image"];
15
+ export interface ModelProfileSource {
16
16
  id: string;
17
17
  name?: string;
18
18
  description?: string;
@@ -21,7 +21,7 @@ interface ModelProfileSource {
21
21
  inputModalities?: ModelModality[];
22
22
  reasoningEfforts?: false | Partial<Record<ReasoningEffort, string | null>>;
23
23
  }
24
- interface ProviderProfileSource {
24
+ export interface ProviderProfileSource {
25
25
  apiKeyEnv?: string;
26
26
  displayName?: string;
27
27
  baseURL: string;
@@ -41,24 +41,18 @@ interface ProviderProfileSource {
41
41
  timeoutMs?: number;
42
42
  retryPolicy?: RetryPolicyConfig;
43
43
  }
44
- interface Config {
45
- /**
46
- * provider 路由,key 是路由键。**volatile**:值经 Loader 的引用读取
47
- * (`config.providers.get()`),设置页改它时不需要重挂这行插件——上游 0.1.7 起
48
- * 「运行期可改」只有这一条路(旧的 settings namespace section 覆盖已取消)。
49
- */
44
+ export interface Config {
50
45
  providers: Volatile<Record<string, ProviderProfileSource>>;
51
46
  }
52
- /** 解析成普通值之后的配置形状(校验与解析只认它)。 */
53
- type Options = { [K in keyof Config]?: Config[K] extends Volatile<infer T> ? T : never; };
54
- declare const Config: z<Schemastery.ObjectS<NoInfer<{
47
+ export type Options = { [K in keyof Config]?: Config[K] extends Volatile<infer T> ? T : never; };
48
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
55
49
  providers: z<NoInfer<Dict<ProviderProfileSource, string>>, NoInfer<Dict<ProviderProfileSource, string>>, "volatile-defined">;
56
50
  }>>, Schemastery.ObjectT<NoInfer<{
57
51
  providers: z<NoInfer<Dict<ProviderProfileSource, string>>, NoInfer<Dict<ProviderProfileSource, string>>, "volatile-defined">;
58
52
  }>>, "plain">;
59
- declare function resolveAdapterOptions(provider: string, source: ProviderProfileSource): ResolvedProviderProfile;
60
- declare function resolveProfiles(providers: Readonly<Record<string, ProviderProfileSource>> | undefined): Map<string, ResolvedProviderProfile>;
61
- declare function assertServiceable(config: Options): void;
62
- declare function apply(ctx: Context, config: Config): void;
53
+ export declare function resolveAdapterOptions(provider: string, source: ProviderProfileSource): ResolvedProviderProfile;
54
+ export declare function resolveProfiles(providers: Readonly<Record<string, ProviderProfileSource>> | undefined): Map<string, ResolvedProviderProfile>;
55
+ export declare function assertServiceable(config: Options): void;
56
+ export declare function apply(ctx: Context, config: Config): void;
63
57
  //#endregion
64
- export { Config, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, MODEL_MODALITIES, ModelProfileSource, NS, OpenAICompatibleAdapter, type OpenAICompatibleAdapterOptions, Options, ProviderProfileSource, REASONING_LEVELS, type ReasoningEffort, type ResolvedModelProfile, type ResolvedProviderProfile, apply, assertServiceable, inject, name, resolveAdapterOptions, resolveProfiles };
58
+ export { DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, OpenAICompatibleAdapter, type OpenAICompatibleAdapterOptions, type ReasoningEffort, type ResolvedModelProfile, type ResolvedProviderProfile };
package/dist/index.mjs CHANGED
@@ -259,10 +259,6 @@ 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
262
  const localized = (text) => text;
267
263
  const modelSchema = z.object({
268
264
  id: z.string().required().description(localized({
@@ -1,7 +1,6 @@
1
1
  import { Context } from "@deepseek-ai/cordis";
2
2
  //#region src/invariant.d.ts
3
- declare const name = "llm-openai-compatible-invariant";
4
- declare const inject: string[];
5
- declare const apply: (ctx: Context) => Promise<() => void>;
6
- //#endregion
7
- export { apply, inject, name };
3
+ export declare const name = "llm-openai-compatible-invariant";
4
+ export declare const inject: string[];
5
+ export declare const apply: (ctx: Context) => Promise<() => void>;
6
+ //#endregion
package/dist/wire.d.mts CHANGED
@@ -3,13 +3,13 @@ import { FinishReason, GenerateOptions, StreamChunk, TokenUsage } from "@deepsee
3
3
  import { LanguageModelV4FinishReason, LanguageModelV4FunctionTool, LanguageModelV4Prompt, LanguageModelV4StreamPart, LanguageModelV4Usage, SharedV4ProviderOptions } from "@ai-sdk/provider";
4
4
  import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
5
5
  //#region src/serialize.d.ts
6
- type OpenAICompatibleProviderOptions = SharedV4ProviderOptions & {
6
+ export type OpenAICompatibleProviderOptions = SharedV4ProviderOptions & {
7
7
  "openai-compatible"?: {
8
8
  reasoningEffort?: string;
9
9
  top_k?: number;
10
10
  };
11
11
  };
12
- interface OpenAICompatibleCallOptions {
12
+ export interface OpenAICompatibleCallOptions {
13
13
  prompt: LanguageModelV4Prompt;
14
14
  maxOutputTokens?: number;
15
15
  temperature?: number;
@@ -21,17 +21,16 @@ interface OpenAICompatibleCallOptions {
21
21
  tools?: LanguageModelV4FunctionTool[];
22
22
  providerOptions?: OpenAICompatibleProviderOptions;
23
23
  }
24
- declare function resolveReasoningWire(model: ResolvedModelProfile | undefined, effort: ResolvedProviderProfile["reasoning"] | undefined): string | undefined;
25
- declare function serializeCallOptions(options: GenerateOptions, profile: ResolvedProviderProfile, model: ResolvedModelProfile | undefined): Promise<OpenAICompatibleCallOptions>;
26
- declare function serializeCallOptionsWithImages(options: GenerateOptions, profile: ResolvedProviderProfile, model: ResolvedModelProfile | undefined, images: {
24
+ export declare function resolveReasoningWire(model: ResolvedModelProfile | undefined, effort: ResolvedProviderProfile["reasoning"] | undefined): string | undefined;
25
+ export declare function serializeCallOptions(options: GenerateOptions, profile: ResolvedProviderProfile, model: ResolvedModelProfile | undefined): Promise<OpenAICompatibleCallOptions>;
26
+ export declare function serializeCallOptionsWithImages(options: GenerateOptions, profile: ResolvedProviderProfile, model: ResolvedModelProfile | undefined, images: {
27
27
  attachments: AttachmentStore;
28
28
  maxRequestImageBytes: number;
29
29
  signal?: AbortSignal;
30
30
  }): Promise<OpenAICompatibleCallOptions>;
31
31
  //#endregion
32
32
  //#region src/translate.d.ts
33
- declare function mapFinishReason(reason: LanguageModelV4FinishReason): FinishReason;
34
- declare function mapUsage(usage: LanguageModelV4Usage): TokenUsage;
35
- declare function translate(stream: ReadableStream<LanguageModelV4StreamPart>): AsyncGenerator<StreamChunk, void>;
36
- //#endregion
37
- export { OpenAICompatibleCallOptions, OpenAICompatibleProviderOptions, mapFinishReason, mapUsage, resolveReasoningWire, serializeCallOptions, serializeCallOptionsWithImages, translate };
33
+ export declare function mapFinishReason(reason: LanguageModelV4FinishReason): FinishReason;
34
+ export declare function mapUsage(usage: LanguageModelV4Usage): TokenUsage;
35
+ export declare function translate(stream: ReadableStream<LanguageModelV4StreamPart>): AsyncGenerator<StreamChunk, void>;
36
+ //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/dsh-llm-openai-compatible",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
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",
package/src/index.ts CHANGED
@@ -82,21 +82,16 @@ export interface ProviderProfileSource {
82
82
  }
83
83
 
84
84
  export interface Config {
85
- /**
86
- * provider 路由,key 是路由键。**volatile**:值经 Loader 的引用读取
87
- * (`config.providers.get()`),设置页改它时不需要重挂这行插件——上游 0.1.7 起
88
- * 「运行期可改」只有这一条路(旧的 settings namespace section 覆盖已取消)。
89
- */
85
+ // provider 路由,key 是路由键。**volatile**:值经 Loader 的引用读取(`config.providers.get()`),
86
+ // 设置页改它不必重挂这行插件——运行期可改只有这一条路。
90
87
  providers: Volatile<Record<string, ProviderProfileSource>>;
91
88
  }
92
89
 
93
- /** 解析成普通值之后的配置形状(校验与解析只认它)。 */
90
+ // 解析成普通值之后的配置形状(校验与解析只认它)。
94
91
  export type Options = { [K in keyof Config]?: Config[K] extends Volatile<infer T> ? T : never };
95
92
 
96
- /**
97
- * 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
98
- * (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
99
- */
93
+ // 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
94
+ // (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
100
95
  const localized = (text: { zh: string; en: string }): string => text as unknown as string;
101
96
 
102
97
  const modelSchema = z.object({
@@ -700,7 +695,7 @@ export function apply(ctx: Context, config: Config): void {
700
695
  }
701
696
  };
702
697
  // volatile 更新(设置页保存)只把新值提交进运行引用并广播,不重挂这一行:这里重算路由注册与
703
- // 可配置 provider 目录(上游 0.1.7 的运行期改配置机制;`profiles()` 读的就是引用里的新值)。
698
+ // 可配置 provider 目录(`profiles()` 读的就是引用里的新值)。
704
699
  ctx.on("loader/volatile-update", refresh);
705
700
  // 候选配置在提交前先校验:无效更新被拒绝,运行引用保持原值(loader 只记录这次拒绝)。
706
701
  ctx.on("internal/config", function (this: Context["fiber"], _raw, next) {
package/src/serialize.ts CHANGED
@@ -258,7 +258,7 @@ async function serializePrompt(
258
258
  continue;
259
259
  }
260
260
  if (message.role === "tool") {
261
- // 结果消息自己带 `toolCallId` 与结果块(V4 起结果不再是 user 消息里的一个块)。
261
+ // 结果消息自己带 `toolCallId` 与结果块(结果不是 user 消息里的一个块)。
262
262
  const images: UserContentPart[] = [];
263
263
  if (resolveImage !== void 0) {
264
264
  for (const block of message.content) {