@morlay/dsh-llm-openai-compatible 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,336 @@
1
+ import {
2
+ LlmError,
3
+ contentHasImage,
4
+ offloadRequestImagesWithPolicy,
5
+ textOnlyImageText,
6
+ } from "@deepseek-ai/dsh-llm";
7
+ import type { ContentBlock, GenerateOptions, Message } from "@deepseek-ai/dsh-llm";
8
+ import { AttachmentError } from "@deepseek-ai/dsh-attachment";
9
+ import type { AttachmentStore } from "@deepseek-ai/dsh-attachment";
10
+ import type {
11
+ JSONSchema7,
12
+ LanguageModelV4FunctionTool,
13
+ LanguageModelV4Prompt,
14
+ SharedV4ProviderOptions,
15
+ } from "@ai-sdk/provider";
16
+ import { Buffer } from "node:buffer";
17
+ import type { ReasoningEffort, ResolvedModelProfile, ResolvedProviderProfile } from "./adapter.ts";
18
+
19
+ const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result:";
20
+
21
+ type UserContentPart = Extract<LanguageModelV4Prompt[number], { role: "user" }>["content"][number];
22
+
23
+ export type OpenAICompatibleProviderOptions = SharedV4ProviderOptions & {
24
+ "openai-compatible"?: {
25
+ reasoningEffort?: string;
26
+
27
+ top_k?: number;
28
+ };
29
+ };
30
+
31
+ export interface OpenAICompatibleCallOptions {
32
+ prompt: LanguageModelV4Prompt;
33
+ maxOutputTokens?: number;
34
+ temperature?: number;
35
+ topP?: number;
36
+ presencePenalty?: number;
37
+ frequencyPenalty?: number;
38
+ seed?: number;
39
+ stopSequences?: string[];
40
+ tools?: LanguageModelV4FunctionTool[];
41
+ providerOptions?: OpenAICompatibleProviderOptions;
42
+ }
43
+
44
+ export function resolveReasoningWire(
45
+ model: ResolvedModelProfile | undefined,
46
+ effort: ResolvedProviderProfile["reasoning"] | undefined,
47
+ ): string | undefined {
48
+ if (effort === void 0) return void 0;
49
+ const declaration = model?.reasoningEfforts;
50
+ if (declaration === void 0 || declaration === false) {
51
+ const subject = model === void 0 ? "unlisted model" : `model "${model.id}"`;
52
+ throw new LlmError(
53
+ `OpenAI-compatible ${subject} declares no reasoning efforts, so "${effort}" cannot be selected`,
54
+ "UNSUPPORTED_REASONING_EFFORT",
55
+ );
56
+ }
57
+ const wire = declaration[effort];
58
+ if (wire === void 0) {
59
+ throw new LlmError(
60
+ `OpenAI-compatible model "${model.id}" does not support reasoning effort "${effort}"`,
61
+ "UNSUPPORTED_REASONING_EFFORT",
62
+ );
63
+ }
64
+ if (wire === null) return void 0;
65
+ return wire;
66
+ }
67
+
68
+ function flattenText(blocks: readonly ContentBlock[]): string {
69
+ return blocks
70
+ .filter((block) => block.type === "text")
71
+ .map((block) => block.text)
72
+ .join("");
73
+ }
74
+
75
+ function assertTextOnly(blocks: readonly ContentBlock[]): void {
76
+ if (contentHasImage(blocks)) {
77
+ throw new LlmError(
78
+ "The OpenAI-compatible chat-completions adapter does not support image content in this message.",
79
+ "UNSUPPORTED_CONTENT",
80
+ );
81
+ }
82
+ }
83
+
84
+ function assertSupportedImageRoles(messages: readonly Message[]): void {
85
+ for (const message of messages) {
86
+ if (message.role !== "user" && contentHasImage(message.content)) {
87
+ throw new LlmError(
88
+ `The OpenAI-compatible chat-completions adapter cannot represent image content in a ${message.role} message.`,
89
+ "UNSUPPORTED_CONTENT",
90
+ );
91
+ }
92
+ }
93
+ }
94
+
95
+ async function imagePart(
96
+ block: Extract<ContentBlock, { type: "image" }>,
97
+ attachments: AttachmentStore,
98
+ signal?: AbortSignal,
99
+ ): Promise<UserContentPart> {
100
+ try {
101
+ const stored = await attachments.readImage(block.attachment, signal);
102
+ return {
103
+ type: "file",
104
+ mediaType: stored.ref.mediaType,
105
+ data: {
106
+ type: "url",
107
+ url: new URL(
108
+ `data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}`,
109
+ ),
110
+ },
111
+ };
112
+ } catch (error) {
113
+ if (error instanceof AttachmentError)
114
+ throw new LlmError(error.message, error.code, { cause: error });
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ function assistantParts(
120
+ message: Message,
121
+ toolNames: Map<string, string>,
122
+ ): Extract<LanguageModelV4Prompt[number], { role: "assistant" }>["content"] {
123
+ const parts: Extract<LanguageModelV4Prompt[number], { role: "assistant" }>["content"] = [];
124
+ for (const block of message.content) {
125
+ switch (block.type) {
126
+ case "text":
127
+ if (block.text.length > 0) parts.push({ type: "text", text: block.text });
128
+ break;
129
+ case "reasoning":
130
+ if (block.text.length > 0) parts.push({ type: "reasoning", text: block.text });
131
+ break;
132
+ case "tool-call": {
133
+ let input: unknown;
134
+ try {
135
+ input = JSON.parse(block.arguments) as unknown;
136
+ } catch {
137
+ throw new LlmError(
138
+ `assistant tool call "${block.id}" carries malformed JSON arguments`,
139
+ "MALFORMED_RESPONSE",
140
+ );
141
+ }
142
+ parts.push({ type: "tool-call", toolCallId: block.id, toolName: block.name, input });
143
+ toolNames.set(block.id, block.name);
144
+ break;
145
+ }
146
+ default:
147
+ break;
148
+ }
149
+ }
150
+ return parts;
151
+ }
152
+
153
+ async function userParts(
154
+ blocks: readonly ContentBlock[],
155
+ resolveImage:
156
+ | ((
157
+ block: Extract<ContentBlock, { type: "image" }>,
158
+ signal?: AbortSignal,
159
+ ) => Promise<UserContentPart>)
160
+ | undefined,
161
+ signal?: AbortSignal,
162
+ ): Promise<UserContentPart[]> {
163
+ const parts: UserContentPart[] = [];
164
+ for (const block of blocks) {
165
+ switch (block.type) {
166
+ case "text":
167
+ if (block.text.length > 0) parts.push({ type: "text", text: block.text });
168
+ break;
169
+ case "image":
170
+ if (resolveImage === void 0)
171
+ throw new LlmError(
172
+ "The OpenAI-compatible chat-completions adapter does not support image content in this message.",
173
+ "UNSUPPORTED_CONTENT",
174
+ );
175
+ parts.push(await resolveImage(block, signal));
176
+ break;
177
+ case "tool-result":
178
+ parts.push(...(await userParts(block.content, resolveImage, signal)));
179
+ break;
180
+ default:
181
+ break;
182
+ }
183
+ }
184
+ return parts;
185
+ }
186
+
187
+ async function serializePrompt(
188
+ messages: readonly Message[],
189
+ resolveImage:
190
+ | ((
191
+ block: Extract<ContentBlock, { type: "image" }>,
192
+ signal?: AbortSignal,
193
+ ) => Promise<UserContentPart>)
194
+ | undefined,
195
+ signal?: AbortSignal,
196
+ ): Promise<LanguageModelV4Prompt> {
197
+ if (resolveImage === void 0) {
198
+ for (const message of messages) assertTextOnly(message.content);
199
+ } else {
200
+ assertSupportedImageRoles(messages);
201
+ }
202
+ const prompt: LanguageModelV4Prompt = [];
203
+ const toolNames = new Map<string, string>();
204
+ let pendingToolImages: UserContentPart[] = [];
205
+ const flushToolImages = () => {
206
+ if (pendingToolImages.length === 0) return;
207
+ prompt.push({
208
+ role: "user",
209
+ content: [{ type: "text", text: TOOL_RESULT_IMAGE_TEXT }, ...pendingToolImages],
210
+ });
211
+ pendingToolImages = [];
212
+ };
213
+ for (const message of messages) {
214
+ if (message.role === "system") {
215
+ flushToolImages();
216
+ prompt.push({ role: "system", content: flattenText(message.content) });
217
+ continue;
218
+ }
219
+ if (message.role === "assistant") {
220
+ flushToolImages();
221
+ const parts = assistantParts(message, toolNames);
222
+ if (parts.length > 0) prompt.push({ role: "assistant", content: parts });
223
+ continue;
224
+ }
225
+ const regular = message.content.filter((block) => block.type !== "tool-result");
226
+ const toolResults = message.content.filter((block) => block.type === "tool-result");
227
+ const content = await userParts(regular, resolveImage, signal);
228
+ if (content.length > 0 || toolResults.length === 0) {
229
+ flushToolImages();
230
+ prompt.push({ role: "user", content });
231
+ }
232
+ for (const result of toolResults) {
233
+ const images: UserContentPart[] = [];
234
+ if (resolveImage !== void 0) {
235
+ for (const block of result.content) {
236
+ if (block.type === "image") images.push(await resolveImage(block, signal));
237
+ }
238
+ }
239
+ prompt.push({
240
+ role: "tool",
241
+ content: [
242
+ {
243
+ type: "tool-result",
244
+ toolCallId: result.toolCallId,
245
+ toolName: toolNames.get(result.toolCallId) ?? "",
246
+ output: {
247
+ type: "text",
248
+ value:
249
+ flattenText(result.content) ||
250
+ (images.length > 0 ? "(see attached image)" : "(no output)"),
251
+ },
252
+ },
253
+ ],
254
+ });
255
+ pendingToolImages.push(...images);
256
+ }
257
+ }
258
+ flushToolImages();
259
+ return prompt;
260
+ }
261
+
262
+ function serializeTools(options: GenerateOptions): LanguageModelV4FunctionTool[] | undefined {
263
+ const tools = options.tools?.map((tool): LanguageModelV4FunctionTool => ({
264
+ type: "function",
265
+ name: tool.name,
266
+ description: tool.description,
267
+ inputSchema: tool.parameters as JSONSchema7,
268
+ }));
269
+ return tools !== void 0 && tools.length > 0 ? tools : void 0;
270
+ }
271
+
272
+ function callOptionsWithPrompt(
273
+ options: GenerateOptions,
274
+ profile: ResolvedProviderProfile,
275
+ model: ResolvedModelProfile | undefined,
276
+ prompt: LanguageModelV4Prompt,
277
+ ): OpenAICompatibleCallOptions {
278
+ const tools = serializeTools(options);
279
+ const temperature = options.temperature ?? profile.temperature;
280
+ const maxOutputTokens = options.maxTokens ?? model?.maxTokens ?? profile.defaultMaxTokens;
281
+ const requestedEffort: ReasoningEffort | undefined =
282
+ options.reasoningEffort === void 0
283
+ ? profile.reasoning
284
+ : (options.reasoningEffort as unknown as ReasoningEffort);
285
+ const reasoningEffort = resolveReasoningWire(model, requestedEffort);
286
+ const providerOptions: OpenAICompatibleProviderOptions = {
287
+ "openai-compatible": {
288
+ ...(reasoningEffort === void 0 ? {} : { reasoningEffort }),
289
+ ...(profile.topK === void 0 ? {} : { top_k: profile.topK }),
290
+ },
291
+ };
292
+ return {
293
+ prompt,
294
+ ...(temperature !== void 0 ? { temperature } : {}),
295
+ ...(profile.topP !== void 0 ? { topP: profile.topP } : {}),
296
+ ...(profile.presencePenalty !== void 0 ? { presencePenalty: profile.presencePenalty } : {}),
297
+ ...(profile.frequencyPenalty !== void 0 ? { frequencyPenalty: profile.frequencyPenalty } : {}),
298
+ ...(profile.seed !== void 0 ? { seed: profile.seed } : {}),
299
+ ...(maxOutputTokens !== void 0 ? { maxOutputTokens } : {}),
300
+ ...(options.stop !== void 0 ? { stopSequences: options.stop } : {}),
301
+ ...(tools !== void 0 ? { tools } : {}),
302
+ ...(Object.keys(providerOptions["openai-compatible"] ?? {}).length > 0
303
+ ? { providerOptions }
304
+ : {}),
305
+ };
306
+ }
307
+
308
+ export async function serializeCallOptions(
309
+ options: GenerateOptions,
310
+ profile: ResolvedProviderProfile,
311
+ model: ResolvedModelProfile | undefined,
312
+ ): Promise<OpenAICompatibleCallOptions> {
313
+ const system =
314
+ options.system === void 0 ? [] : [{ role: "system" as const, content: options.system }];
315
+ const prompt = await serializePrompt(options.messages, void 0);
316
+ return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);
317
+ }
318
+
319
+ export async function serializeCallOptionsWithImages(
320
+ options: GenerateOptions,
321
+ profile: ResolvedProviderProfile,
322
+ model: ResolvedModelProfile | undefined,
323
+ images: { attachments: AttachmentStore; maxRequestImageBytes: number; signal?: AbortSignal },
324
+ ): Promise<OpenAICompatibleCallOptions> {
325
+ const requestMessages = offloadRequestImagesWithPolicy(options.messages, {
326
+ representation: "raw",
327
+ maxBytes: images.maxRequestImageBytes,
328
+ placeholder: (ref) => textOnlyImageText(ref),
329
+ });
330
+ const resolveImage = (block: Extract<ContentBlock, { type: "image" }>, signal?: AbortSignal) =>
331
+ imagePart(block, images.attachments, signal);
332
+ const system =
333
+ options.system === void 0 ? [] : [{ role: "system" as const, content: options.system }];
334
+ const prompt = await serializePrompt(requestMessages, resolveImage, images.signal);
335
+ return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);
336
+ }
@@ -0,0 +1,195 @@
1
+ import { EMPTY_RESPONSE_CODE, ToolCallId, LlmError } from "@deepseek-ai/dsh-llm";
2
+ import type { FinishReason, StreamChunk, TokenUsage } from "@deepseek-ai/dsh-llm";
3
+ import type {
4
+ LanguageModelV4FinishReason,
5
+ LanguageModelV4StreamPart,
6
+ LanguageModelV4ToolCall,
7
+ LanguageModelV4Usage,
8
+ } from "@ai-sdk/provider";
9
+
10
+ export function mapFinishReason(reason: LanguageModelV4FinishReason): FinishReason {
11
+ switch (reason.unified) {
12
+ case "stop":
13
+ return { kind: "stop" };
14
+ case "tool-calls":
15
+ return { kind: "tool-calls" };
16
+ case "length":
17
+ return { kind: "max-tokens" };
18
+ default:
19
+ return {
20
+ kind: "error",
21
+ failure: {
22
+ message: `model stopped: ${reason.raw ?? reason.unified}`,
23
+ code: (reason.raw ?? reason.unified).toUpperCase(),
24
+ },
25
+ };
26
+ }
27
+ }
28
+
29
+ export function mapUsage(usage: LanguageModelV4Usage): TokenUsage {
30
+ const cacheRead = usage.inputTokens.cacheRead;
31
+ const reasoning = usage.outputTokens.reasoning;
32
+ return {
33
+ inputTokens: usage.inputTokens.noCache ?? usage.inputTokens.total ?? 0,
34
+ outputTokens: usage.outputTokens.total ?? 0,
35
+ ...(cacheRead !== void 0 && cacheRead > 0 ? { cacheReadTokens: cacheRead } : {}),
36
+ ...(reasoning !== void 0 && reasoning > 0 ? { reasoningTokens: reasoning } : {}),
37
+ };
38
+ }
39
+
40
+ interface OpenBlock {
41
+ index: number;
42
+ kind: "text" | "reasoning" | "tool-call";
43
+ text: string;
44
+ callId?: string;
45
+ name?: string;
46
+ }
47
+
48
+ function closeBlock(block: OpenBlock): Extract<StreamChunk, { type: "block-end" }>["block"] {
49
+ switch (block.kind) {
50
+ case "text":
51
+ return { type: "text", text: block.text };
52
+ case "reasoning":
53
+ return { type: "reasoning", text: block.text };
54
+ case "tool-call":
55
+ return {
56
+ type: "tool-call",
57
+ id: ToolCallId(block.callId ?? ""),
58
+ name: block.name ?? "",
59
+ arguments: block.text,
60
+ };
61
+ }
62
+ }
63
+
64
+ export async function* translate(
65
+ stream: ReadableStream<LanguageModelV4StreamPart>,
66
+ ): AsyncGenerator<StreamChunk, void> {
67
+ let nextIndex = 0;
68
+ const textBlocks = new Map<string, OpenBlock>();
69
+ const reasoningBlocks = new Map<string, OpenBlock>();
70
+ const toolBlocks = new Map<string, OpenBlock>();
71
+ const toolQueue: OpenBlock[] = [];
72
+ const order: OpenBlock[] = [];
73
+ let pendingUsage: TokenUsage | undefined;
74
+ let pendingFinish: FinishReason | undefined;
75
+
76
+ const open = (kind: OpenBlock["kind"]): OpenBlock => {
77
+ const block: OpenBlock = { index: nextIndex++, kind, text: "" };
78
+ order.push(block);
79
+ return block;
80
+ };
81
+
82
+ for await (const part of stream) {
83
+ switch (part.type) {
84
+ case "stream-start":
85
+ case "response-metadata":
86
+ case "raw":
87
+ break;
88
+ case "text-start": {
89
+ const block = open("text");
90
+ textBlocks.set(part.id, block);
91
+ yield { type: "block-start", index: block.index, blockType: "text" };
92
+ break;
93
+ }
94
+ case "text-delta": {
95
+ const block = textBlocks.get(part.id);
96
+ if (block === void 0) break;
97
+ block.text += part.delta;
98
+ yield { type: "text-delta", index: block.index, text: part.delta };
99
+ break;
100
+ }
101
+ case "text-end":
102
+ break;
103
+ case "reasoning-start": {
104
+ const block = open("reasoning");
105
+ reasoningBlocks.set(part.id, block);
106
+ yield { type: "block-start", index: block.index, blockType: "reasoning" };
107
+ break;
108
+ }
109
+ case "reasoning-delta": {
110
+ const block = reasoningBlocks.get(part.id);
111
+ if (block === void 0) break;
112
+ block.text += part.delta;
113
+ yield { type: "reasoning-delta", index: block.index, text: part.delta };
114
+ break;
115
+ }
116
+ case "reasoning-end":
117
+ break;
118
+ case "tool-input-start": {
119
+ const block = open("tool-call");
120
+ if (part.toolName !== void 0) block.name = part.toolName;
121
+ toolBlocks.set(part.id, block);
122
+ toolQueue.push(block);
123
+ yield { type: "block-start", index: block.index, blockType: "tool-call" };
124
+ break;
125
+ }
126
+ case "tool-input-delta": {
127
+ const block = toolBlocks.get(part.id);
128
+ if (block === void 0) break;
129
+ block.text += part.delta;
130
+ yield {
131
+ type: "tool-call-delta",
132
+ index: block.index,
133
+ id: ToolCallId(block.callId ?? part.id),
134
+ ...(block.name !== void 0 ? { name: block.name } : {}),
135
+ argumentsDelta: part.delta,
136
+ };
137
+ break;
138
+ }
139
+ case "tool-input-end":
140
+ break;
141
+ case "tool-call": {
142
+ applyToolCall(part, toolQueue);
143
+ break;
144
+ }
145
+ case "tool-result":
146
+ case "tool-approval-request":
147
+ case "custom":
148
+ case "file":
149
+ case "reasoning-file":
150
+ case "source":
151
+ // Provider-executed tools and generated files are not part of this
152
+ // adapter's client-executed tool loop; nothing to emit.
153
+ break;
154
+ case "finish":
155
+ pendingUsage = mapUsage(part.usage);
156
+ pendingFinish = mapFinishReason(part.finishReason);
157
+ for (const block of order)
158
+ yield { type: "block-end", index: block.index, block: closeBlock(block) };
159
+ if (pendingUsage !== void 0) yield { type: "usage", usage: pendingUsage };
160
+ const reason = pendingFinish ?? { kind: "stop" as const };
161
+ yield {
162
+ type: "finish",
163
+ reason:
164
+ reason.kind === "stop" && order.length === 0
165
+ ? {
166
+ kind: "error",
167
+ failure: {
168
+ message: "model returned a completed response with no content",
169
+ code: EMPTY_RESPONSE_CODE,
170
+ },
171
+ }
172
+ : reason,
173
+ };
174
+ return;
175
+ case "error": {
176
+ const error = part.error;
177
+ const cause = error instanceof Error ? error : void 0;
178
+ const message =
179
+ cause?.message ?? (typeof error === "string" ? error : "provider stream error");
180
+ throw new LlmError(`OpenAI-compatible stream failed: ${message}`, "TRANSPORT", { cause });
181
+ }
182
+ }
183
+ }
184
+ throw new LlmError("AI SDK stream ended without a finish part", "STREAM_CLOSED");
185
+ }
186
+
187
+ function applyToolCall(part: LanguageModelV4ToolCall, toolQueue: OpenBlock[]): void {
188
+ const block = toolQueue.shift();
189
+ if (block === void 0) return;
190
+ block.callId = part.toolCallId;
191
+ block.name = part.toolName;
192
+ // The provider emits the complete arguments on this part; the buffered
193
+ // deltas were a partial view.
194
+ block.text = part.input;
195
+ }
package/lib/adapter.d.mts DELETED
@@ -1,137 +0,0 @@
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
@@ -1 +0,0 @@
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"}