@morlay/dsh-llm-openai-compatible 0.0.1

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 ADDED
@@ -0,0 +1,134 @@
1
+ # @morlay/dsh-llm-openai-compatible
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 默认值。用
7
+ `providers` dict 多路由结构(与 `llm-pi-ai` 一致),用户可将现有
8
+ `llm-pi-ai` 配置近乎无缝迁移。
9
+
10
+ 传输层复用 **[@ai-sdk/openai-compatible](https://www.npmjs.com/package/@ai-sdk/openai-compatible)**
11
+ (`LanguageModelV4.doStream`:wire 序列化与 SSE 解析由 SDK 负责);本插件负责
12
+ harness 消息 → AI SDK prompt 转换、采样默认合并、stream part → `StreamChunk`
13
+ 翻译、错误归一化与凭据策略。
14
+
15
+ ## 安装
16
+
17
+ ```sh
18
+ dsh plugin --profile web add "@morlay/dsh-llm-openai-compatible"
19
+ ```
20
+
21
+ 插件默认休眠(空 `providers`):不声明任何 provider 路由,直到你在
22
+ `settings.yaml` 中配置。
23
+
24
+ ## 配置
25
+
26
+ `providers` 是 dict:**key 就是 provider 路由键**(选择器与
27
+ `GenerateOptions.provider` 使用),值是 profile。
28
+
29
+ ```yaml
30
+ llm-openai-compatible:
31
+ providers:
32
+ ollama:
33
+ apiKeyEnv: OLLAMA_API_KEY
34
+ baseURL: https://ollama.com/v1
35
+ displayName: Ollama Gateway
36
+ # === 采样默认参数(请求级 temperature 优先)===
37
+ temperature: 1 # 0..2
38
+ topP: 0.95 # 0..1 → wire top_p
39
+ topK: 40 # 正整数 → wire top_k(非标准,仅网关支持时发送)
40
+ presencePenalty: 0 # -2..2 → wire presence_penalty
41
+ frequencyPenalty: 0 # -2..2 → wire frequency_penalty
42
+ seed: 42 # 正整数 → wire seed
43
+ # === 推理 ===
44
+ reasoning: high # 部署默认档位(省略 = 提供方默认)
45
+ # === 模型目录 ===
46
+ defaultContextWindow: 262144
47
+ defaultMaxTokens: 32768
48
+ models:
49
+ - id: deepseek-v4-flash:0731
50
+ name: DeepSeek V4 Flash
51
+ contextWindow: 1000000
52
+ maxTokens: 65535
53
+ inputModalities: [text, image]
54
+ reasoningEfforts:
55
+ off: # off 空值 = 不发送 reasoning_effort
56
+ high: high # 档位 → wire reasoning_effort 拼写
57
+ max: max
58
+ # === 传输 ===
59
+ maxRequestImageBytes: 20971520
60
+ streamIdleTimeoutMs: 300000
61
+ timeoutMs: 600000 # 整体请求超时;缺省不设
62
+ retryPolicy:
63
+ mode: normal
64
+ maxRetries: 5
65
+ ```
66
+
67
+ ### 采样默认值合并规则
68
+
69
+ | wire 字段 | 取值 | 省略语义 |
70
+ | ----------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------- |
71
+ | `temperature` | `options.temperature ?? profile.temperature` | 都不给 → 不发送,提供方默认 |
72
+ | `max_tokens` | `options.maxTokens ?? model.maxTokens ?? profile.defaultMaxTokens` | 都不给 → 不发送 |
73
+ | `top_p` / `presence_penalty` / `frequency_penalty` / `seed` | `profile` 值 | undefined → 不发送 |
74
+ | `top_k` | `profile.topK`,经 `providerOptions` 透传进请求体 | undefined → 不发送 |
75
+ | `reasoning_effort` | 见下 | 解析不出 → 不发送 |
76
+
77
+ ### reasoning 映射(OpenAI 风格)
78
+
79
+ - 模型声明 `reasoningEfforts`(对象)后,该模型的选器公开 `efforts`(按声明
80
+ 顺序)+ `defaultEffort`(= `profile.reasoning`,须在模型能力内,否则视为无
81
+ 默认值——**描述模型时绝不抛错**)。
82
+ - wire:非 `off` 档位发送 `reasoning_effort: <声明值>`;`off` → 不发送。
83
+ - 请求级 `options.reasoningEffort` 不在模型能力内 → 网络 I/O 前抛
84
+ `LlmError('UNSUPPORTED_REASONING_EFFORT')`。`profile.reasoning` 配了模型不
85
+ 支持的档位 → 请求执行处失败(同一错误码),配置页面仍可编辑。
86
+ - 模型不声明 `reasoningEfforts`(或 `false`)→ 不公开 reasoning 能力。
87
+
88
+ ### 模型目录
89
+
90
+ - `models` 缺省 = 服务**空目录**:`listModels` 返回空,未列出的 id 原样透传
91
+ (`resolveModel` 返回基础信息 + `defaultContextWindow` / `defaultMaxTokens`)。
92
+ - 模型 `maxTokens` 配置后成为该模型的 per-request 默认输出上限。
93
+ - `inputModalities` 缺省 `[text]`;声明含 `image` 的模型接受图片输入
94
+ (attachments seam,base64 data-URL parts)。
95
+
96
+ ### 凭据
97
+
98
+ - profile 设置 `apiKeyEnv` 后:`ctx.credentials` 优先,其次
99
+ `launchEnvironmentOf(ctx)`;解析不到 → `LlmError('MISSING_CREDENTIAL')`。
100
+ - profile 不设置 `apiKeyEnv` → 请求不带 `authorization` 头(无认证端点,
101
+ 如本地 Ollama)。
102
+
103
+ ## 传输
104
+
105
+ - 端点 = `baseURL` + `/chat/completions`(streaming,`stream_options.include_usage`)。
106
+ - 每个请求携带 `attributionHeaders()` + `x-…-harness-user-id`(+ session-id /
107
+ compaction 标头),并带 SDK 的 `ai-sdk/openai-compatible` user-agent 后缀。
108
+ - `streamIdleTimeoutMs` 控制流空闲超时(`TIMEOUT`);`timeoutMs` 控制整体请求
109
+ 超时(缺省不设)。
110
+ - 错误映射:401/403 → `AUTH`、429 → `RATE_LIMIT`、400+上下文 →
111
+ `CONTEXT_WINDOW_EXCEEDED`、5xx → `SERVER`、配额 → `QUOTA_EXCEEDED`。
112
+ - 用量:`prompt_tokens_details.cached_tokens` 与 DeepSeek 方言的
113
+ `prompt_cache_hit_tokens` 都被拆出为 `cacheReadTokens`(disjoint 计数)。
114
+
115
+ ## 从 `llm-pi-ai` 迁移
116
+
117
+ 把 `llm-pi-ai.providers.<route>` 的 `api`/`baseURL`/`models`/采样字段平移到
118
+ `llm-openai-compatible.providers.<route>`,`apiKeyEnv` 与 `retryPolicy` 原样
119
+ 保留;`reasoningEfforts` 的 `off` 空值语义一致。
120
+
121
+ ## 暂缓能力(YAGNI)
122
+
123
+ - 不做 `modelOverrides`(providers 里每个路由自己写 `models` 即可)。
124
+ - 不做模型 discovery(端点询问 `GET /models`);需要时手写 `models`。
125
+ - 不做 OAuth / 非 bearer 认证。
126
+
127
+ ## 构建与验证
128
+
129
+ ```bash
130
+ pnpm install
131
+ pnpm --filter @morlay/dsh-llm-openai-compatible run prepare # → lib/*.mjs + *.d.mts
132
+ pnpm exec tsc --noEmit
133
+ pnpm exec vitest run
134
+ ```
@@ -0,0 +1,24 @@
1
+ - insert:
2
+ - id: llm-openai-compatible
3
+ name: "@morlay/dsh-llm-openai-compatible"
4
+ config:
5
+ providers:
6
+ ollama:
7
+ apiKeyEnv: OLLAMA_API_KEY
8
+ baseURL: https://ollama.com/v1
9
+ displayName: Ollama Gateway
10
+ temperature: 1 # 0..2
11
+ topP: 0.95
12
+ reasoning: high
13
+ defaultContextWindow: 1000000
14
+ defaultMaxTokens: 65535
15
+ models:
16
+ - id: deepseek-v4-flash:0731
17
+ name: "DeepSeek V4 Flash: 0731"
18
+ contextWindow: 1000000
19
+ maxTokens: 65535
20
+ inputModalities: [text]
21
+ reasoningEfforts:
22
+ off: # off 空值 = 不发送 reasoning_effort
23
+ high: high
24
+ max: max
@@ -0,0 +1,137 @@
1
+ import { GenerateOptions, LlmAdapter, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ModelModality, ResolvedRetryPolicy, StreamChunk } from "@deepseek-ai/dsh-llm";
2
+ import { CredentialRef } from "@deepseek-ai/dsh-credentials";
3
+ import { LanguageModelV4Usage } from "@ai-sdk/provider";
4
+ import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
5
+ //#region src/adapter.d.ts
6
+ /** Default maximum idle interval while an adapter stream read is outstanding. */
7
+ declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000;
8
+ /** Default combined request/response context capacity for unconfigured models. */
9
+ declare const DEFAULT_CONTEXT_WINDOW = 262144;
10
+ /** Default per-request output-token cap for unconfigured models. */
11
+ declare const DEFAULT_MAX_TOKENS = 32768;
12
+ /** Default bound on accumulated base64 image payload per request. */
13
+ declare const DEFAULT_MAX_REQUEST_IMAGE_BYTES: number;
14
+ /** Code stamped on the idle-watchdog timeout reason. */
15
+ declare const STREAM_IDLE_TIMEOUT_CODE = "LLM_STREAM_IDLE_TIMEOUT";
16
+ /** Code stamped on the whole-request deadline timeout reason. */
17
+ declare const REQUEST_TIMEOUT_CODE = "LLM_REQUEST_TIMEOUT";
18
+ /** The provider options key the SDK forwards into the request body. */
19
+ declare const PROVIDER_OPTIONS_KEY = "openai-compatible";
20
+ /** Selectable reasoning effort levels for one provider route. */
21
+ type ReasoningEffort = "off" | "low" | "high" | "max";
22
+ /** One validated catalog model of a provider route. */
23
+ interface ResolvedModelProfile {
24
+ id: string;
25
+ name?: string;
26
+ description?: string;
27
+ contextWindow?: number;
28
+ maxTokens?: number;
29
+ inputModalities: readonly ModelModality[];
30
+ /**
31
+ * Declared reasoning efforts: key = selectable level, value = wire
32
+ * `reasoning_effort` spelling. `false` rejects the capability outright;
33
+ * absent means the model carries no reasoning metadata. A `null` wire
34
+ * spelling (only legal for `off`) means "omit the field".
35
+ */
36
+ reasoningEfforts?: false | Partial<Record<ReasoningEffort, string | null>>;
37
+ }
38
+ /** One validated provider route profile, detached and ready for per-request reads. */
39
+ interface ResolvedProviderProfile {
40
+ provider: string;
41
+ displayName: string;
42
+ /** Credential reference; absence means the route sends no authorization header. */
43
+ apiKeyEnv?: CredentialRef;
44
+ /** Required endpoint base; requests hit `${baseURL}/chat/completions`. */
45
+ baseURL: string;
46
+ headers?: Readonly<Record<string, string>>;
47
+ temperature?: number;
48
+ topP?: number;
49
+ topK?: number;
50
+ presencePenalty?: number;
51
+ frequencyPenalty?: number;
52
+ seed?: number;
53
+ /** Deployment default reasoning level; omission keeps the provider default. */
54
+ reasoning?: ReasoningEffort;
55
+ models: readonly ResolvedModelProfile[];
56
+ defaultContextWindow: number;
57
+ defaultMaxTokens: number;
58
+ maxRequestImageBytes: number;
59
+ streamIdleTimeoutMs: number;
60
+ /** Whole-request deadline in milliseconds; unset arms no overall timer. */
61
+ timeoutMs?: number;
62
+ retryPolicy: ResolvedRetryPolicy;
63
+ }
64
+ /** Constructor options for {@link OpenAICompatibleAdapter}: the hooks the plugin owns. */
65
+ interface OpenAICompatibleAdapterOptions {
66
+ /** Current validated profiles by provider route; called once per operation. */
67
+ profiles: () => ReadonlyMap<string, ResolvedProviderProfile>;
68
+ /**
69
+ * Resolve the credential for one already-resolved profile; called once per
70
+ * stream call and frozen for that call. `undefined` means the route sends no
71
+ * authorization header (an unauthenticated endpoint such as local Ollama).
72
+ */
73
+ resolveApiKey: (provider: string, profile: ResolvedProviderProfile) => Promise<string | undefined>;
74
+ /** Resolve the harness anonymous user id for request attribution headers. */
75
+ resolveUserId: () => string;
76
+ /** Resolve the optional durable attachment service at request time. */
77
+ resolveAttachments?: () => AttachmentStore | undefined;
78
+ }
79
+ /** The wire usage shape the SDK converter receives (subset of the OpenAI shape). */
80
+ interface WireUsageLike {
81
+ prompt_tokens?: number | null | undefined;
82
+ completion_tokens?: number | null | undefined;
83
+ prompt_tokens_details?: {
84
+ cached_tokens?: number | null | undefined;
85
+ } | null | undefined;
86
+ /** DeepSeek-dialect cache hits folded into prompt_tokens. */
87
+ prompt_cache_hit_tokens?: number | null | undefined;
88
+ completion_tokens_details?: {
89
+ reasoning_tokens?: number | null | undefined;
90
+ } | null | undefined;
91
+ }
92
+ /**
93
+ * Convert provider token accounting into disjoint AI SDK usage. OpenAI's
94
+ * `prompt_tokens_details.cached_tokens` and the DeepSeek dialect's
95
+ * `prompt_cache_hit_tokens` both report cache reads folded into
96
+ * `prompt_tokens`; the harness convention is disjoint counts, so cache reads
97
+ * are split out regardless of which field the endpoint used.
98
+ */
99
+ declare function convertUsage(usage: WireUsageLike | null | undefined): LanguageModelV4Usage;
100
+ /**
101
+ * Map an HTTP status to a stable LlmError code.
102
+ * @param status - status of a non-2xx provider response.
103
+ * @param error - parsed provider error body, when available.
104
+ * @returns the normalized harness error code.
105
+ */
106
+ declare function httpErrorCode(status: number, error?: {
107
+ code?: unknown;
108
+ type?: unknown;
109
+ message?: unknown;
110
+ }): string;
111
+ /**
112
+ * Multi-provider adapter. Each operation reads the current profiles, so a
113
+ * configuration change reaches the next request without a restart; the
114
+ * underlying SDK provider instance is cached per resolved profile and rebuilt
115
+ * when the profile object changes.
116
+ */
117
+ declare class OpenAICompatibleAdapter extends LlmAdapter {
118
+ private readonly config;
119
+ private readonly sdkProviders;
120
+ constructor(config: OpenAICompatibleAdapterOptions);
121
+ /** The profile for one route, or the not-owned failure. */
122
+ private profileOf;
123
+ /** The configured descriptor for one exact route/model pair; unlisted ids pass through. */
124
+ private modelOf;
125
+ /** The SDK chat model for one route/model, cached per resolved profile. */
126
+ private sdkModel;
127
+ providerInfo(provider: string): LlmProviderInfo;
128
+ providerRetryPolicy(provider: string): ResolvedRetryPolicy | undefined;
129
+ listModels(provider: string): Promise<readonly LlmModelInfo[]>;
130
+ resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
131
+ stream(options: GenerateOptions): AsyncGenerator<StreamChunk>;
132
+ /** Normalize an SDK/transport failure into a harness LlmError. */
133
+ private normalizeTransportError;
134
+ }
135
+ //#endregion
136
+ export { DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, OpenAICompatibleAdapter, OpenAICompatibleAdapterOptions, PROVIDER_OPTIONS_KEY, REQUEST_TIMEOUT_CODE, ReasoningEffort, ResolvedModelProfile, ResolvedProviderProfile, STREAM_IDLE_TIMEOUT_CODE, convertUsage, httpErrorCode };
137
+ //# sourceMappingURL=adapter.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.mts","names":[],"sources":["../src/adapter.ts"],"mappings":";;;;;;cA4Ca;;cAEA;;cAEA;;cAEA;;cAEA;;cAEA;;cAEA;;KAGD;;UAGK;EACf;EACA;EACA;EACA;EACA;EACA,0BAA0B;;;;;;;EAO1B,2BAA2B,QAAQ,OAAO;;;UAI3B;EACf;EACA;;EAEA,YAAY;;EAEZ;EACA,UAAU,SAAS;EAEnB;EACA;EACA;EACA;EACA;EACA;;EAEA,YAAY;EAEZ,iBAAiB;EACjB;EACA;EAEA;EACA;;EAEA;EACA,aAAa;;;UAIE;;EAEf,gBAAgB,oBAAoB;;;;;;EAMpC,gBACE,kBACA,SAAS,4BACN;;EAEL;;EAEA,2BAA2B;;;UAInB;EACR;EACA;EACA;IAA0B;;;EAE1B;EACA;IAA8B;;;;;;;;;;iBAUhB,aAAa,OAAO,mCAAmC;;;;;;;iBAqEvD,cACd,gBACA;EAAU;EAAgB;EAAgB;;;;;;;;cA2C/B,gCAAgC;mBAC1B;mBACA;EAEL,YAAA,QAAQ;;UAMZ;;UAWA;;UAQA;EAqBR,aAAa,mBAAmB;EAOhC,oBAAoB,mBAAmB;EAIvC,WAAW,mBAAmB,iBAAiB;EAK/C,aACE,kBACA,eACA,UAAU,cACT,QAAQ;EAeJ,OAAO,SAAS,kBAAkB,eAAe;;UAoHhD"}
@@ -0,0 +1,295 @@
1
+ import { serializeCallOptions, serializeCallOptionsWithImages } from "./serialize.mjs";
2
+ import { translate } from "./translate.mjs";
3
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
4
+ import { deadline, idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
5
+ import { APICallError } from "@ai-sdk/provider";
6
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
7
+ //#region src/adapter.ts
8
+ /**
9
+ * `OpenAICompatibleAdapter`: a multi-provider harness adapter built on
10
+ * `@ai-sdk/openai-compatible`. One instance serves every provider route in
11
+ * the plugin's `providers` dict; profile facts arrive through a thunk resolved
12
+ * once per operation, so the registering plugin owns validation, layering, and
13
+ * credential policy, and a changed profile reaches the next request without a
14
+ * restart. The SDK owns wire serialization and SSE parsing; this adapter owns
15
+ * harness message conversion, sampling-default merging (via
16
+ * `serialize.ts`), chunk translation (via `translate.ts`), and error
17
+ * normalization.
18
+ * @module dsh-llm-openai-compatible/adapter
19
+ */
20
+ /** Default maximum idle interval while an adapter stream read is outstanding. */
21
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
22
+ /** Default combined request/response context capacity for unconfigured models. */
23
+ const DEFAULT_CONTEXT_WINDOW = 262144;
24
+ /** Default per-request output-token cap for unconfigured models. */
25
+ const DEFAULT_MAX_TOKENS = 32768;
26
+ /** Default bound on accumulated base64 image payload per request. */
27
+ const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20971520;
28
+ /** Code stamped on the idle-watchdog timeout reason. */
29
+ const STREAM_IDLE_TIMEOUT_CODE = "LLM_STREAM_IDLE_TIMEOUT";
30
+ /** Code stamped on the whole-request deadline timeout reason. */
31
+ const REQUEST_TIMEOUT_CODE = "LLM_REQUEST_TIMEOUT";
32
+ /** The provider options key the SDK forwards into the request body. */
33
+ const PROVIDER_OPTIONS_KEY = "openai-compatible";
34
+ /**
35
+ * Convert provider token accounting into disjoint AI SDK usage. OpenAI's
36
+ * `prompt_tokens_details.cached_tokens` and the DeepSeek dialect's
37
+ * `prompt_cache_hit_tokens` both report cache reads folded into
38
+ * `prompt_tokens`; the harness convention is disjoint counts, so cache reads
39
+ * are split out regardless of which field the endpoint used.
40
+ */
41
+ function convertUsage(usage) {
42
+ if (usage == null) return {
43
+ inputTokens: {
44
+ total: 0,
45
+ noCache: 0,
46
+ cacheRead: void 0,
47
+ cacheWrite: void 0
48
+ },
49
+ outputTokens: {
50
+ total: 0,
51
+ text: void 0,
52
+ reasoning: void 0
53
+ }
54
+ };
55
+ const promptTokens = usage.prompt_tokens ?? 0;
56
+ const completionTokens = usage.completion_tokens ?? 0;
57
+ const cacheRead = usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0;
58
+ const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? 0;
59
+ return {
60
+ inputTokens: {
61
+ total: promptTokens,
62
+ noCache: Math.max(0, promptTokens - cacheRead),
63
+ cacheRead,
64
+ cacheWrite: void 0
65
+ },
66
+ outputTokens: {
67
+ total: completionTokens,
68
+ text: Math.max(0, completionTokens - reasoningTokens),
69
+ reasoning: reasoningTokens
70
+ }
71
+ };
72
+ }
73
+ /** The first real blocks of one resolve: display + catalog metadata. */
74
+ function modelInfo(profile, model) {
75
+ return {
76
+ provider: profile.provider,
77
+ id: model.id,
78
+ name: model.name ?? model.id,
79
+ ...model.description === void 0 ? {} : { description: model.description },
80
+ inputModalities: [...model.inputModalities]
81
+ };
82
+ }
83
+ /** The harness reasoning info for one model, honoring its declared efforts. */
84
+ function reasoningInfo(model, defaultEffort) {
85
+ const declaration = model?.reasoningEfforts;
86
+ if (declaration === void 0 || declaration === false) return {};
87
+ return { reasoning: {
88
+ efforts: Object.entries(declaration).map(([id]) => ({
89
+ id: ReasoningEffortId(id),
90
+ name: `${id.charAt(0).toUpperCase()}${id.slice(1)}`
91
+ })),
92
+ ...defaultEffort !== void 0 && declaration[defaultEffort] !== void 0 ? { defaultEffort: ReasoningEffortId(defaultEffort) } : {}
93
+ } };
94
+ }
95
+ /**
96
+ * Map an HTTP status to a stable LlmError code.
97
+ * @param status - status of a non-2xx provider response.
98
+ * @param error - parsed provider error body, when available.
99
+ * @returns the normalized harness error code.
100
+ */
101
+ function httpErrorCode(status, error) {
102
+ if (status === 401 || status === 403) return "AUTH";
103
+ if (status === 413) return "INVALID_REQUEST";
104
+ const detail = [
105
+ error?.code,
106
+ error?.type,
107
+ error?.message
108
+ ].filter(Boolean).join(" ");
109
+ if (isQuotaExceededError(detail)) return QUOTA_EXCEEDED_CODE;
110
+ if (status === 429) return "RATE_LIMIT";
111
+ if (status === 400) {
112
+ if (isContextWindowExceededError(detail)) return CONTEXT_WINDOW_EXCEEDED_CODE;
113
+ return "INVALID_REQUEST";
114
+ }
115
+ if (status >= 500) return "SERVER";
116
+ return `HTTP_${status}`;
117
+ }
118
+ /** Parse a provider error body out of an API-call error's JSON response body. */
119
+ function providerErrorBody(error) {
120
+ if (error.responseBody === void 0) return void 0;
121
+ try {
122
+ return JSON.parse(error.responseBody).error;
123
+ } catch {
124
+ return;
125
+ }
126
+ }
127
+ /** Extract a provider-issued request id from response headers when present. */
128
+ function requestId(headers) {
129
+ if (headers === void 0) return void 0;
130
+ const value = headers["x-request-id"] ?? headers["x-openai-compatible-request-id"];
131
+ return value === void 0 || value.length === 0 ? void 0 : ProviderRequestId(value);
132
+ }
133
+ /**
134
+ * Multi-provider adapter. Each operation reads the current profiles, so a
135
+ * configuration change reaches the next request without a restart; the
136
+ * underlying SDK provider instance is cached per resolved profile and rebuilt
137
+ * when the profile object changes.
138
+ */
139
+ var OpenAICompatibleAdapter = class extends LlmAdapter {
140
+ config;
141
+ sdkProviders = /* @__PURE__ */ new Map();
142
+ constructor(config) {
143
+ super();
144
+ this.config = config;
145
+ }
146
+ /** The profile for one route, or the not-owned failure. */
147
+ profileOf(provider) {
148
+ const profile = this.config.profiles().get(provider);
149
+ if (profile === void 0) throw new LlmError(`OpenAI-compatible adapter does not own provider "${provider}"`, "NO_ADAPTER");
150
+ return profile;
151
+ }
152
+ /** The configured descriptor for one exact route/model pair; unlisted ids pass through. */
153
+ modelOf(profile, model) {
154
+ return profile.models.find((entry) => entry.id === model);
155
+ }
156
+ /** The SDK chat model for one route/model, cached per resolved profile. */
157
+ sdkModel(profile, modelId) {
158
+ let byModel = this.sdkProviders.get(profile);
159
+ if (byModel === void 0) {
160
+ byModel = /* @__PURE__ */ new Map();
161
+ this.sdkProviders.set(profile, byModel);
162
+ }
163
+ let model = byModel.get(modelId);
164
+ if (model === void 0) {
165
+ model = createOpenAICompatible({
166
+ name: PROVIDER_OPTIONS_KEY,
167
+ baseURL: profile.baseURL,
168
+ headers: {
169
+ ...profile.headers,
170
+ ...attributionHeaders()
171
+ },
172
+ includeUsage: true,
173
+ convertUsage
174
+ }).chatModel(modelId);
175
+ byModel.set(modelId, model);
176
+ }
177
+ return model;
178
+ }
179
+ providerInfo(provider) {
180
+ return {
181
+ id: provider,
182
+ name: this.config.profiles().get(provider)?.displayName ?? provider
183
+ };
184
+ }
185
+ providerRetryPolicy(provider) {
186
+ return this.config.profiles().get(provider)?.retryPolicy;
187
+ }
188
+ listModels(provider) {
189
+ const profile = this.profileOf(provider);
190
+ return Promise.resolve(profile.models.map((model) => modelInfo(profile, model)));
191
+ }
192
+ resolveModel(provider, model, _signal) {
193
+ const profile = this.profileOf(provider);
194
+ const configured = this.modelOf(profile, model);
195
+ const contextWindow = configured?.contextWindow ?? profile.defaultContextWindow;
196
+ const maxTokens = configured?.maxTokens ?? profile.defaultMaxTokens;
197
+ return Promise.resolve({
198
+ ...configured === void 0 ? {
199
+ provider,
200
+ id: model,
201
+ name: model,
202
+ inputModalities: ["text"]
203
+ } : modelInfo(profile, configured),
204
+ context: { contextWindow },
205
+ ...maxTokens !== void 0 ? { defaultMaxTokens: maxTokens } : {},
206
+ ...reasoningInfo(configured, profile.reasoning)
207
+ });
208
+ }
209
+ async *stream(options) {
210
+ const profile = this.profileOf(options.provider);
211
+ const model = this.modelOf(profile, options.model);
212
+ const hasImages = options.messages.some((message) => contentHasImage(message.content));
213
+ let attachments;
214
+ if (hasImages) {
215
+ if (model?.inputModalities.includes("image") !== true) throw new LlmError(`OpenAI-compatible model "${options.model}" does not accept image input.`, "UNSUPPORTED_CONTENT");
216
+ attachments = this.config.resolveAttachments?.();
217
+ if (attachments === void 0) throw new LlmError("OpenAI-compatible image conversion requires the durable attachment service.", "UNSUPPORTED_CONTENT");
218
+ }
219
+ const apiKey = await this.config.resolveApiKey(options.provider, profile);
220
+ const userId = this.config.resolveUserId();
221
+ const consumer = new AbortController();
222
+ const upstream = options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]);
223
+ const overall = profile.timeoutMs === void 0 ? void 0 : deadline(upstream, profile.timeoutMs, REQUEST_TIMEOUT_CODE);
224
+ const watchdog = idleWatchdog(overall?.signal ?? upstream, profile.streamIdleTimeoutMs, STREAM_IDLE_TIMEOUT_CODE);
225
+ try {
226
+ const callOptions = attachments === void 0 ? await serializeCallOptions(options, profile, model) : await serializeCallOptionsWithImages(options, profile, model, {
227
+ attachments,
228
+ maxRequestImageBytes: profile.maxRequestImageBytes,
229
+ signal: watchdog.signal
230
+ });
231
+ const sdkModel = this.sdkModel(profile, options.model);
232
+ let result;
233
+ try {
234
+ result = await sdkModel.doStream({
235
+ ...callOptions,
236
+ abortSignal: watchdog.signal,
237
+ headers: {
238
+ ...apiKey === void 0 ? {} : { authorization: `Bearer ${apiKey}` },
239
+ "x-openai-compatible-harness-user-id": String(userId),
240
+ ...options.sessionId !== void 0 ? { "x-openai-compatible-harness-session-id": String(options.sessionId) } : {},
241
+ ...options.purpose === "compaction" ? { "x-openai-compatible-harness-compact": "1" } : {}
242
+ }
243
+ });
244
+ } catch (error) {
245
+ throw this.normalizeTransportError(error, profile);
246
+ }
247
+ const iterator = translate(result.stream)[Symbol.asyncIterator]();
248
+ let exhausted = false;
249
+ try {
250
+ while (true) {
251
+ const next = await watchdog.next(iterator);
252
+ if (next.done) {
253
+ exhausted = true;
254
+ return;
255
+ }
256
+ yield next.value;
257
+ }
258
+ } catch (error) {
259
+ if (timeoutOf(watchdog.signal, "LLM_STREAM_IDLE_TIMEOUT") !== void 0) throw new LlmError(`OpenAI-compatible stream idle timeout after ${profile.streamIdleTimeoutMs}ms`, "TIMEOUT", { cause: error });
260
+ if (profile.timeoutMs !== void 0 && timeoutOf(watchdog.signal, "LLM_REQUEST_TIMEOUT") !== void 0) throw new LlmError(`OpenAI-compatible request timeout after ${profile.timeoutMs}ms`, "TIMEOUT", { cause: error });
261
+ if (options.signal?.aborted) throw new LlmError("OpenAI-compatible request aborted by caller", "ABORTED", { cause: error });
262
+ if (error instanceof LlmError) throw error;
263
+ throw this.normalizeTransportError(error, profile);
264
+ } finally {
265
+ consumer.abort("OpenAI-compatible stream consumer stopped");
266
+ if (!exhausted) try {
267
+ await iterator.return(void 0);
268
+ } catch {}
269
+ }
270
+ } finally {
271
+ watchdog[Symbol.dispose]();
272
+ overall?.[Symbol.dispose]();
273
+ }
274
+ }
275
+ /** Normalize an SDK/transport failure into a harness LlmError. */
276
+ normalizeTransportError(error, profile) {
277
+ if (error instanceof LlmError) return error;
278
+ if (APICallError.isInstance(error)) {
279
+ const providerError = providerErrorBody(error);
280
+ const message = typeof providerError?.message === "string" ? providerError.message : error.message;
281
+ const id = requestId(error.responseHeaders);
282
+ return new LlmError(message, httpErrorCode(error.statusCode ?? 0, providerError), {
283
+ ...error.statusCode === void 0 ? {} : { status: error.statusCode },
284
+ ...id === void 0 ? {} : { requestId: id },
285
+ cause: error
286
+ });
287
+ }
288
+ if (error instanceof Error) return new LlmError(`OpenAI-compatible API request to ${profile.baseURL} failed`, "TRANSPORT", { cause: error });
289
+ return new LlmError(`OpenAI-compatible API request to ${profile.baseURL} failed`, "TRANSPORT");
290
+ }
291
+ };
292
+ //#endregion
293
+ export { DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, OpenAICompatibleAdapter, PROVIDER_OPTIONS_KEY, REQUEST_TIMEOUT_CODE, STREAM_IDLE_TIMEOUT_CODE, convertUsage, httpErrorCode };
294
+
295
+ //# sourceMappingURL=adapter.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.mjs","names":[],"sources":["../src/adapter.ts"],"sourcesContent":["/**\n * `OpenAICompatibleAdapter`: a multi-provider harness adapter built on\n * `@ai-sdk/openai-compatible`. One instance serves every provider route in\n * the plugin's `providers` dict; profile facts arrive through a thunk resolved\n * once per operation, so the registering plugin owns validation, layering, and\n * credential policy, and a changed profile reaches the next request without a\n * restart. The SDK owns wire serialization and SSE parsing; this adapter owns\n * harness message conversion, sampling-default merging (via\n * `serialize.ts`), chunk translation (via `translate.ts`), and error\n * normalization.\n * @module dsh-llm-openai-compatible/adapter\n */\n\nimport {\n CONTEXT_WINDOW_EXCEEDED_CODE,\n QUOTA_EXCEEDED_CODE,\n LlmAdapter,\n LlmError,\n ProviderRequestId,\n ReasoningEffortId,\n attributionHeaders,\n contentHasImage,\n isContextWindowExceededError,\n isQuotaExceededError,\n} from \"@deepseek-ai/dsh-llm\";\nimport type {\n GenerateOptions,\n LlmModelInfo,\n LlmProviderInfo,\n LlmResolvedModelInfo,\n ModelModality,\n ResolvedRetryPolicy,\n StreamChunk,\n} from \"@deepseek-ai/dsh-llm\";\nimport type { AttachmentStore } from \"@deepseek-ai/dsh-attachment\";\nimport type { CredentialRef } from \"@deepseek-ai/dsh-credentials\";\nimport { deadline, idleWatchdog, timeoutOf } from \"@deepseek-ai/dsh-timeout\";\nimport { APICallError } from \"@ai-sdk/provider\";\nimport type { LanguageModelV4, LanguageModelV4Usage } from \"@ai-sdk/provider\";\nimport { createOpenAICompatible } from \"@ai-sdk/openai-compatible\";\nimport { serializeCallOptions, serializeCallOptionsWithImages } from \"./serialize.ts\";\nimport { translate } from \"./translate.ts\";\n\n/** Default maximum idle interval while an adapter stream read is outstanding. */\nexport const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000;\n/** Default combined request/response context capacity for unconfigured models. */\nexport const DEFAULT_CONTEXT_WINDOW = 262_144;\n/** Default per-request output-token cap for unconfigured models. */\nexport const DEFAULT_MAX_TOKENS = 32_768;\n/** Default bound on accumulated base64 image payload per request. */\nexport const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;\n/** Code stamped on the idle-watchdog timeout reason. */\nexport const STREAM_IDLE_TIMEOUT_CODE = \"LLM_STREAM_IDLE_TIMEOUT\";\n/** Code stamped on the whole-request deadline timeout reason. */\nexport const REQUEST_TIMEOUT_CODE = \"LLM_REQUEST_TIMEOUT\";\n/** The provider options key the SDK forwards into the request body. */\nexport const PROVIDER_OPTIONS_KEY = \"openai-compatible\";\n\n/** Selectable reasoning effort levels for one provider route. */\nexport type ReasoningEffort = \"off\" | \"low\" | \"high\" | \"max\";\n\n/** One validated catalog model of a provider route. */\nexport interface ResolvedModelProfile {\n id: string;\n name?: string;\n description?: string;\n contextWindow?: number;\n maxTokens?: number;\n inputModalities: readonly ModelModality[];\n /**\n * Declared reasoning efforts: key = selectable level, value = wire\n * `reasoning_effort` spelling. `false` rejects the capability outright;\n * absent means the model carries no reasoning metadata. A `null` wire\n * spelling (only legal for `off`) means \"omit the field\".\n */\n reasoningEfforts?: false | Partial<Record<ReasoningEffort, string | null>>;\n}\n\n/** One validated provider route profile, detached and ready for per-request reads. */\nexport interface ResolvedProviderProfile {\n provider: string;\n displayName: string;\n /** Credential reference; absence means the route sends no authorization header. */\n apiKeyEnv?: CredentialRef;\n /** Required endpoint base; requests hit `${baseURL}/chat/completions`. */\n baseURL: string;\n headers?: Readonly<Record<string, string>>;\n // === sampling defaults (request-level values win) ===\n temperature?: number;\n topP?: number;\n topK?: number;\n presencePenalty?: number;\n frequencyPenalty?: number;\n seed?: number;\n /** Deployment default reasoning level; omission keeps the provider default. */\n reasoning?: ReasoningEffort;\n // === model catalog ===\n models: readonly ResolvedModelProfile[];\n defaultContextWindow: number;\n defaultMaxTokens: number;\n // === transport ===\n maxRequestImageBytes: number;\n streamIdleTimeoutMs: number;\n /** Whole-request deadline in milliseconds; unset arms no overall timer. */\n timeoutMs?: number;\n retryPolicy: ResolvedRetryPolicy;\n}\n\n/** Constructor options for {@link OpenAICompatibleAdapter}: the hooks the plugin owns. */\nexport interface OpenAICompatibleAdapterOptions {\n /** Current validated profiles by provider route; called once per operation. */\n profiles: () => ReadonlyMap<string, ResolvedProviderProfile>;\n /**\n * Resolve the credential for one already-resolved profile; called once per\n * stream call and frozen for that call. `undefined` means the route sends no\n * authorization header (an unauthenticated endpoint such as local Ollama).\n */\n resolveApiKey: (\n provider: string,\n profile: ResolvedProviderProfile,\n ) => Promise<string | undefined>;\n /** Resolve the harness anonymous user id for request attribution headers. */\n resolveUserId: () => string;\n /** Resolve the optional durable attachment service at request time. */\n resolveAttachments?: () => AttachmentStore | undefined;\n}\n\n/** The wire usage shape the SDK converter receives (subset of the OpenAI shape). */\ninterface WireUsageLike {\n prompt_tokens?: number | null | undefined;\n completion_tokens?: number | null | undefined;\n prompt_tokens_details?: { cached_tokens?: number | null | undefined } | null | undefined;\n /** DeepSeek-dialect cache hits folded into prompt_tokens. */\n prompt_cache_hit_tokens?: number | null | undefined;\n completion_tokens_details?: { reasoning_tokens?: number | null | undefined } | null | undefined;\n}\n\n/**\n * Convert provider token accounting into disjoint AI SDK usage. OpenAI's\n * `prompt_tokens_details.cached_tokens` and the DeepSeek dialect's\n * `prompt_cache_hit_tokens` both report cache reads folded into\n * `prompt_tokens`; the harness convention is disjoint counts, so cache reads\n * are split out regardless of which field the endpoint used.\n */\nexport function convertUsage(usage: WireUsageLike | null | undefined): LanguageModelV4Usage {\n if (usage == null) {\n return {\n inputTokens: { total: 0, noCache: 0, cacheRead: void 0, cacheWrite: void 0 },\n outputTokens: { total: 0, text: void 0, reasoning: void 0 },\n };\n }\n const promptTokens = usage.prompt_tokens ?? 0;\n const completionTokens = usage.completion_tokens ?? 0;\n const cacheRead =\n usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0;\n const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? 0;\n return {\n inputTokens: {\n total: promptTokens,\n noCache: Math.max(0, promptTokens - cacheRead),\n cacheRead,\n cacheWrite: void 0,\n },\n outputTokens: {\n total: completionTokens,\n text: Math.max(0, completionTokens - reasoningTokens),\n reasoning: reasoningTokens,\n },\n };\n}\n\n/** The first real blocks of one resolve: display + catalog metadata. */\nfunction modelInfo(profile: ResolvedProviderProfile, model: ResolvedModelProfile): LlmModelInfo {\n return {\n provider: profile.provider,\n id: model.id,\n name: model.name ?? model.id,\n ...(model.description === void 0 ? {} : { description: model.description }),\n inputModalities: [...model.inputModalities],\n };\n}\n\n/** The harness reasoning info for one model, honoring its declared efforts. */\nfunction reasoningInfo(\n model: ResolvedModelProfile | undefined,\n defaultEffort: ReasoningEffort | undefined,\n): Pick<LlmResolvedModelInfo, \"reasoning\"> {\n const declaration = model?.reasoningEfforts;\n if (declaration === void 0 || declaration === false) return {};\n const entries = Object.entries(declaration) as [ReasoningEffort, string | null | undefined][];\n const efforts = entries.map(([id]) => ({\n id: ReasoningEffortId(id),\n name: `${id.charAt(0).toUpperCase()}${id.slice(1)}`,\n }));\n return {\n reasoning: {\n efforts,\n // A configured default the model does not declare is silently dropped\n // here (describing a model must never throw); the request path still\n // refuses it, which is where a bad deployment default belongs.\n ...(defaultEffort !== void 0 && declaration[defaultEffort] !== void 0\n ? { defaultEffort: ReasoningEffortId(defaultEffort) }\n : {}),\n },\n };\n}\n\n/**\n * Map an HTTP status to a stable LlmError code.\n * @param status - status of a non-2xx provider response.\n * @param error - parsed provider error body, when available.\n * @returns the normalized harness error code.\n */\nexport function httpErrorCode(\n status: number,\n error?: { code?: unknown; type?: unknown; message?: unknown },\n): string {\n if (status === 401 || status === 403) return \"AUTH\";\n if (status === 413) return \"INVALID_REQUEST\";\n const detail = [error?.code, error?.type, error?.message].filter(Boolean).join(\" \");\n if (isQuotaExceededError(detail)) return QUOTA_EXCEEDED_CODE;\n if (status === 429) return \"RATE_LIMIT\";\n if (status === 400) {\n if (isContextWindowExceededError(detail)) return CONTEXT_WINDOW_EXCEEDED_CODE;\n return \"INVALID_REQUEST\";\n }\n if (status >= 500) return \"SERVER\";\n return `HTTP_${status}`;\n}\n\n/** Parse a provider error body out of an API-call error's JSON response body. */\nfunction providerErrorBody(\n error: APICallError,\n): { code?: unknown; type?: unknown; message?: unknown } | undefined {\n if (error.responseBody === void 0) return void 0;\n try {\n const parsed = JSON.parse(error.responseBody) as {\n error?: { code?: unknown; type?: unknown; message?: unknown };\n };\n return parsed.error;\n } catch {\n return void 0;\n }\n}\n\n/** Extract a provider-issued request id from response headers when present. */\nfunction requestId(headers: Record<string, string> | undefined): ProviderRequestId | undefined {\n if (headers === void 0) return void 0;\n const value = headers[\"x-request-id\"] ?? headers[\"x-openai-compatible-request-id\"];\n return value === void 0 || value.length === 0 ? void 0 : ProviderRequestId(value);\n}\n\n/**\n * Multi-provider adapter. Each operation reads the current profiles, so a\n * configuration change reaches the next request without a restart; the\n * underlying SDK provider instance is cached per resolved profile and rebuilt\n * when the profile object changes.\n */\nexport class OpenAICompatibleAdapter extends LlmAdapter {\n private readonly config: OpenAICompatibleAdapterOptions;\n private readonly sdkProviders = new Map<ResolvedProviderProfile, Map<string, LanguageModelV4>>();\n\n constructor(config: OpenAICompatibleAdapterOptions) {\n super();\n this.config = config;\n }\n\n /** The profile for one route, or the not-owned failure. */\n private profileOf(provider: string): ResolvedProviderProfile {\n const profile = this.config.profiles().get(provider);\n if (profile === void 0)\n throw new LlmError(\n `OpenAI-compatible adapter does not own provider \"${provider}\"`,\n \"NO_ADAPTER\",\n );\n return profile;\n }\n\n /** The configured descriptor for one exact route/model pair; unlisted ids pass through. */\n private modelOf(\n profile: ResolvedProviderProfile,\n model: string,\n ): ResolvedModelProfile | undefined {\n return profile.models.find((entry) => entry.id === model);\n }\n\n /** The SDK chat model for one route/model, cached per resolved profile. */\n private sdkModel(profile: ResolvedProviderProfile, modelId: string): LanguageModelV4 {\n let byModel = this.sdkProviders.get(profile);\n if (byModel === void 0) {\n byModel = new Map();\n this.sdkProviders.set(profile, byModel);\n }\n let model = byModel.get(modelId);\n if (model === void 0) {\n const provider = createOpenAICompatible({\n name: PROVIDER_OPTIONS_KEY,\n baseURL: profile.baseURL,\n headers: { ...profile.headers, ...attributionHeaders() },\n includeUsage: true,\n convertUsage,\n });\n model = provider.chatModel(modelId);\n byModel.set(modelId, model);\n }\n return model;\n }\n\n providerInfo(provider: string): LlmProviderInfo {\n return {\n id: provider,\n name: this.config.profiles().get(provider)?.displayName ?? provider,\n };\n }\n\n providerRetryPolicy(provider: string): ResolvedRetryPolicy | undefined {\n return this.config.profiles().get(provider)?.retryPolicy;\n }\n\n listModels(provider: string): Promise<readonly LlmModelInfo[]> {\n const profile = this.profileOf(provider);\n return Promise.resolve(profile.models.map((model) => modelInfo(profile, model)));\n }\n\n resolveModel(\n provider: string,\n model: string,\n _signal?: AbortSignal,\n ): Promise<LlmResolvedModelInfo> {\n const profile = this.profileOf(provider);\n const configured = this.modelOf(profile, model);\n const contextWindow = configured?.contextWindow ?? profile.defaultContextWindow;\n const maxTokens = configured?.maxTokens ?? profile.defaultMaxTokens;\n return Promise.resolve({\n ...(configured === void 0\n ? { provider, id: model, name: model, inputModalities: [\"text\" as const] }\n : modelInfo(profile, configured)),\n context: { contextWindow },\n ...(maxTokens !== void 0 ? { defaultMaxTokens: maxTokens } : {}),\n ...reasoningInfo(configured, profile.reasoning),\n });\n }\n\n async *stream(options: GenerateOptions): AsyncGenerator<StreamChunk> {\n const profile = this.profileOf(options.provider);\n const model = this.modelOf(profile, options.model);\n const hasImages = options.messages.some((message) => contentHasImage(message.content));\n let attachments: AttachmentStore | undefined;\n if (hasImages) {\n if (model?.inputModalities.includes(\"image\") !== true) {\n throw new LlmError(\n `OpenAI-compatible model \"${options.model}\" does not accept image input.`,\n \"UNSUPPORTED_CONTENT\",\n );\n }\n attachments = this.config.resolveAttachments?.();\n if (attachments === void 0)\n throw new LlmError(\n \"OpenAI-compatible image conversion requires the durable attachment service.\",\n \"UNSUPPORTED_CONTENT\",\n );\n }\n const apiKey = await this.config.resolveApiKey(options.provider, profile);\n const userId = this.config.resolveUserId();\n const consumer = new AbortController();\n const upstream =\n options.signal === void 0\n ? consumer.signal\n : AbortSignal.any([options.signal, consumer.signal]);\n const overall =\n profile.timeoutMs === void 0\n ? void 0\n : deadline(upstream, profile.timeoutMs, REQUEST_TIMEOUT_CODE);\n const watchdog = idleWatchdog(\n overall?.signal ?? upstream,\n profile.streamIdleTimeoutMs,\n STREAM_IDLE_TIMEOUT_CODE,\n );\n try {\n const callOptions =\n attachments === void 0\n ? await serializeCallOptions(options, profile, model)\n : await serializeCallOptionsWithImages(options, profile, model, {\n attachments,\n maxRequestImageBytes: profile.maxRequestImageBytes,\n signal: watchdog.signal,\n });\n const sdkModel = this.sdkModel(profile, options.model);\n let result;\n try {\n result = await sdkModel.doStream({\n ...callOptions,\n abortSignal: watchdog.signal,\n headers: {\n ...(apiKey === void 0 ? {} : { authorization: `Bearer ${apiKey}` }),\n \"x-openai-compatible-harness-user-id\": String(userId),\n ...(options.sessionId !== void 0\n ? { \"x-openai-compatible-harness-session-id\": String(options.sessionId) }\n : {}),\n ...(options.purpose === \"compaction\"\n ? { \"x-openai-compatible-harness-compact\": \"1\" }\n : {}),\n },\n });\n } catch (error) {\n throw this.normalizeTransportError(error, profile);\n }\n const iterator = translate(result.stream)[Symbol.asyncIterator]();\n let exhausted = false;\n try {\n while (true) {\n const next = await watchdog.next(iterator);\n if (next.done) {\n exhausted = true;\n return;\n }\n yield next.value;\n }\n } catch (error) {\n if (timeoutOf(watchdog.signal, STREAM_IDLE_TIMEOUT_CODE) !== void 0) {\n throw new LlmError(\n `OpenAI-compatible stream idle timeout after ${profile.streamIdleTimeoutMs}ms`,\n \"TIMEOUT\",\n { cause: error },\n );\n }\n if (\n profile.timeoutMs !== void 0 &&\n timeoutOf(watchdog.signal, REQUEST_TIMEOUT_CODE) !== void 0\n ) {\n throw new LlmError(\n `OpenAI-compatible request timeout after ${profile.timeoutMs}ms`,\n \"TIMEOUT\",\n { cause: error },\n );\n }\n if (options.signal?.aborted)\n throw new LlmError(\"OpenAI-compatible request aborted by caller\", \"ABORTED\", {\n cause: error,\n });\n if (error instanceof LlmError) throw error;\n throw this.normalizeTransportError(error, profile);\n } finally {\n consumer.abort(\"OpenAI-compatible stream consumer stopped\");\n if (!exhausted) {\n try {\n await iterator.return(void 0);\n } catch {\n // The transport already aborted; teardown is best-effort.\n }\n }\n }\n } finally {\n watchdog[Symbol.dispose]();\n overall?.[Symbol.dispose]();\n }\n }\n\n /** Normalize an SDK/transport failure into a harness LlmError. */\n private normalizeTransportError(error: unknown, profile: ResolvedProviderProfile): LlmError {\n if (error instanceof LlmError) return error;\n if (APICallError.isInstance(error)) {\n const providerError = providerErrorBody(error);\n const message =\n typeof providerError?.message === \"string\" ? providerError.message : error.message;\n const id = requestId(error.responseHeaders);\n return new LlmError(message, httpErrorCode(error.statusCode ?? 0, providerError), {\n ...(error.statusCode === void 0 ? {} : { status: error.statusCode }),\n ...(id === void 0 ? {} : { requestId: id }),\n cause: error,\n });\n }\n if (error instanceof Error) {\n return new LlmError(\n `OpenAI-compatible API request to ${profile.baseURL} failed`,\n \"TRANSPORT\",\n { cause: error },\n );\n }\n return new LlmError(`OpenAI-compatible API request to ${profile.baseURL} failed`, \"TRANSPORT\");\n }\n}\n"],"mappings":"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"}