@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.
- package/LICENSE +21 -0
- package/README.md +0 -9
- package/cordis.patch.yml +3 -3
- package/dist/index.d.mts +109 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +895 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +19 -18
- package/src/adapter.ts +426 -0
- package/src/index.ts +497 -0
- package/src/serialize.ts +336 -0
- package/src/translate.ts +195 -0
- package/lib/adapter.d.mts +0 -137
- package/lib/adapter.d.mts.map +0 -1
- package/lib/adapter.mjs +0 -295
- package/lib/adapter.mjs.map +0 -1
- package/lib/index.d.mts +0 -89
- package/lib/index.d.mts.map +0 -1
- package/lib/index.mjs +0 -293
- package/lib/index.mjs.map +0 -1
- package/lib/serialize.d.mts +0 -68
- package/lib/serialize.d.mts.map +0 -1
- package/lib/serialize.mjs +0 -276
- package/lib/serialize.mjs.map +0 -1
- package/lib/translate.d.mts +0 -19
- package/lib/translate.d.mts.map +0 -1
- package/lib/translate.mjs +0 -209
- package/lib/translate.mjs.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, RetryPolicySchema, ToolCallId, assertUsableApiKey, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError, offloadRequestImagesWithPolicy, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
|
+
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
5
|
+
import { deepEqualJson } from "@deepseek-ai/dsh-util-values";
|
|
6
|
+
import { MAX_TIMER_DELAY_MS, deadline, idleWatchdog, timeoutOf } from "@deepseek-ai/dsh-timeout";
|
|
7
|
+
import { getOrCreateAnonymousUserId } from "@deepseek-ai/dsh-anonymous-user-id";
|
|
8
|
+
import { APICallError } from "@ai-sdk/provider";
|
|
9
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
10
|
+
import { AttachmentError } from "@deepseek-ai/dsh-attachment";
|
|
11
|
+
import { Buffer } from "node:buffer";
|
|
12
|
+
//#region src/serialize.ts
|
|
13
|
+
const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result:";
|
|
14
|
+
function resolveReasoningWire(model, effort) {
|
|
15
|
+
if (effort === void 0) return void 0;
|
|
16
|
+
const declaration = model?.reasoningEfforts;
|
|
17
|
+
if (declaration === void 0 || declaration === false) {
|
|
18
|
+
const subject = model === void 0 ? "unlisted model" : `model "${model.id}"`;
|
|
19
|
+
throw new LlmError(`OpenAI-compatible ${subject} declares no reasoning efforts, so "${effort}" cannot be selected`, "UNSUPPORTED_REASONING_EFFORT");
|
|
20
|
+
}
|
|
21
|
+
const wire = declaration[effort];
|
|
22
|
+
if (wire === void 0) throw new LlmError(`OpenAI-compatible model "${model.id}" does not support reasoning effort "${effort}"`, "UNSUPPORTED_REASONING_EFFORT");
|
|
23
|
+
if (wire === null) return void 0;
|
|
24
|
+
return wire;
|
|
25
|
+
}
|
|
26
|
+
function flattenText(blocks) {
|
|
27
|
+
return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
28
|
+
}
|
|
29
|
+
function assertTextOnly(blocks) {
|
|
30
|
+
if (contentHasImage(blocks)) throw new LlmError("The OpenAI-compatible chat-completions adapter does not support image content in this message.", "UNSUPPORTED_CONTENT");
|
|
31
|
+
}
|
|
32
|
+
function assertSupportedImageRoles(messages) {
|
|
33
|
+
for (const message of messages) if (message.role !== "user" && contentHasImage(message.content)) throw new LlmError(`The OpenAI-compatible chat-completions adapter cannot represent image content in a ${message.role} message.`, "UNSUPPORTED_CONTENT");
|
|
34
|
+
}
|
|
35
|
+
async function imagePart(block, attachments, signal) {
|
|
36
|
+
try {
|
|
37
|
+
const stored = await attachments.readImage(block.attachment, signal);
|
|
38
|
+
return {
|
|
39
|
+
type: "file",
|
|
40
|
+
mediaType: stored.ref.mediaType,
|
|
41
|
+
data: {
|
|
42
|
+
type: "url",
|
|
43
|
+
url: new URL(`data:${stored.ref.mediaType};base64,${Buffer.from(stored.data).toString("base64")}`)
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof AttachmentError) throw new LlmError(error.message, error.code, { cause: error });
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function assistantParts(message, toolNames) {
|
|
52
|
+
const parts = [];
|
|
53
|
+
for (const block of message.content) switch (block.type) {
|
|
54
|
+
case "text":
|
|
55
|
+
if (block.text.length > 0) parts.push({
|
|
56
|
+
type: "text",
|
|
57
|
+
text: block.text
|
|
58
|
+
});
|
|
59
|
+
break;
|
|
60
|
+
case "reasoning":
|
|
61
|
+
if (block.text.length > 0) parts.push({
|
|
62
|
+
type: "reasoning",
|
|
63
|
+
text: block.text
|
|
64
|
+
});
|
|
65
|
+
break;
|
|
66
|
+
case "tool-call": {
|
|
67
|
+
let input;
|
|
68
|
+
try {
|
|
69
|
+
input = JSON.parse(block.arguments);
|
|
70
|
+
} catch {
|
|
71
|
+
throw new LlmError(`assistant tool call "${block.id}" carries malformed JSON arguments`, "MALFORMED_RESPONSE");
|
|
72
|
+
}
|
|
73
|
+
parts.push({
|
|
74
|
+
type: "tool-call",
|
|
75
|
+
toolCallId: block.id,
|
|
76
|
+
toolName: block.name,
|
|
77
|
+
input
|
|
78
|
+
});
|
|
79
|
+
toolNames.set(block.id, block.name);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return parts;
|
|
84
|
+
}
|
|
85
|
+
async function userParts(blocks, resolveImage, signal) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
for (const block of blocks) switch (block.type) {
|
|
88
|
+
case "text":
|
|
89
|
+
if (block.text.length > 0) parts.push({
|
|
90
|
+
type: "text",
|
|
91
|
+
text: block.text
|
|
92
|
+
});
|
|
93
|
+
break;
|
|
94
|
+
case "image":
|
|
95
|
+
if (resolveImage === void 0) throw new LlmError("The OpenAI-compatible chat-completions adapter does not support image content in this message.", "UNSUPPORTED_CONTENT");
|
|
96
|
+
parts.push(await resolveImage(block, signal));
|
|
97
|
+
break;
|
|
98
|
+
case "tool-result": parts.push(...await userParts(block.content, resolveImage, signal));
|
|
99
|
+
}
|
|
100
|
+
return parts;
|
|
101
|
+
}
|
|
102
|
+
async function serializePrompt(messages, resolveImage, signal) {
|
|
103
|
+
if (resolveImage === void 0) for (const message of messages) assertTextOnly(message.content);
|
|
104
|
+
else assertSupportedImageRoles(messages);
|
|
105
|
+
const prompt = [];
|
|
106
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
107
|
+
let pendingToolImages = [];
|
|
108
|
+
const flushToolImages = () => {
|
|
109
|
+
if (pendingToolImages.length === 0) return;
|
|
110
|
+
prompt.push({
|
|
111
|
+
role: "user",
|
|
112
|
+
content: [{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: TOOL_RESULT_IMAGE_TEXT
|
|
115
|
+
}, ...pendingToolImages]
|
|
116
|
+
});
|
|
117
|
+
pendingToolImages = [];
|
|
118
|
+
};
|
|
119
|
+
for (const message of messages) {
|
|
120
|
+
if (message.role === "system") {
|
|
121
|
+
flushToolImages();
|
|
122
|
+
prompt.push({
|
|
123
|
+
role: "system",
|
|
124
|
+
content: flattenText(message.content)
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (message.role === "assistant") {
|
|
129
|
+
flushToolImages();
|
|
130
|
+
const parts = assistantParts(message, toolNames);
|
|
131
|
+
if (parts.length > 0) prompt.push({
|
|
132
|
+
role: "assistant",
|
|
133
|
+
content: parts
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const regular = message.content.filter((block) => block.type !== "tool-result");
|
|
138
|
+
const toolResults = message.content.filter((block) => block.type === "tool-result");
|
|
139
|
+
const content = await userParts(regular, resolveImage, signal);
|
|
140
|
+
if (content.length > 0 || toolResults.length === 0) {
|
|
141
|
+
flushToolImages();
|
|
142
|
+
prompt.push({
|
|
143
|
+
role: "user",
|
|
144
|
+
content
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
for (const result of toolResults) {
|
|
148
|
+
const images = [];
|
|
149
|
+
if (resolveImage !== void 0) {
|
|
150
|
+
for (const block of result.content) if (block.type === "image") images.push(await resolveImage(block, signal));
|
|
151
|
+
}
|
|
152
|
+
prompt.push({
|
|
153
|
+
role: "tool",
|
|
154
|
+
content: [{
|
|
155
|
+
type: "tool-result",
|
|
156
|
+
toolCallId: result.toolCallId,
|
|
157
|
+
toolName: toolNames.get(result.toolCallId) ?? "",
|
|
158
|
+
output: {
|
|
159
|
+
type: "text",
|
|
160
|
+
value: flattenText(result.content) || (images.length > 0 ? "(see attached image)" : "(no output)")
|
|
161
|
+
}
|
|
162
|
+
}]
|
|
163
|
+
});
|
|
164
|
+
pendingToolImages.push(...images);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
flushToolImages();
|
|
168
|
+
return prompt;
|
|
169
|
+
}
|
|
170
|
+
function serializeTools(options) {
|
|
171
|
+
const tools = options.tools?.map((tool) => ({
|
|
172
|
+
type: "function",
|
|
173
|
+
name: tool.name,
|
|
174
|
+
description: tool.description,
|
|
175
|
+
inputSchema: tool.parameters
|
|
176
|
+
}));
|
|
177
|
+
return tools !== void 0 && tools.length > 0 ? tools : void 0;
|
|
178
|
+
}
|
|
179
|
+
function callOptionsWithPrompt(options, profile, model, prompt) {
|
|
180
|
+
const tools = serializeTools(options);
|
|
181
|
+
const temperature = options.temperature ?? profile.temperature;
|
|
182
|
+
const maxOutputTokens = options.maxTokens ?? model?.maxTokens ?? profile.defaultMaxTokens;
|
|
183
|
+
const reasoningEffort = resolveReasoningWire(model, options.reasoningEffort === void 0 ? profile.reasoning : options.reasoningEffort);
|
|
184
|
+
const providerOptions = { "openai-compatible": {
|
|
185
|
+
...reasoningEffort === void 0 ? {} : { reasoningEffort },
|
|
186
|
+
...profile.topK === void 0 ? {} : { top_k: profile.topK }
|
|
187
|
+
} };
|
|
188
|
+
return {
|
|
189
|
+
prompt,
|
|
190
|
+
...temperature !== void 0 ? { temperature } : {},
|
|
191
|
+
...profile.topP !== void 0 ? { topP: profile.topP } : {},
|
|
192
|
+
...profile.presencePenalty !== void 0 ? { presencePenalty: profile.presencePenalty } : {},
|
|
193
|
+
...profile.frequencyPenalty !== void 0 ? { frequencyPenalty: profile.frequencyPenalty } : {},
|
|
194
|
+
...profile.seed !== void 0 ? { seed: profile.seed } : {},
|
|
195
|
+
...maxOutputTokens !== void 0 ? { maxOutputTokens } : {},
|
|
196
|
+
...options.stop !== void 0 ? { stopSequences: options.stop } : {},
|
|
197
|
+
...tools !== void 0 ? { tools } : {},
|
|
198
|
+
...Object.keys(providerOptions["openai-compatible"] ?? {}).length > 0 ? { providerOptions } : {}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function serializeCallOptions(options, profile, model) {
|
|
202
|
+
const system = options.system === void 0 ? [] : [{
|
|
203
|
+
role: "system",
|
|
204
|
+
content: options.system
|
|
205
|
+
}];
|
|
206
|
+
const prompt = await serializePrompt(options.messages, void 0);
|
|
207
|
+
return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);
|
|
208
|
+
}
|
|
209
|
+
async function serializeCallOptionsWithImages(options, profile, model, images) {
|
|
210
|
+
const requestMessages = offloadRequestImagesWithPolicy(options.messages, {
|
|
211
|
+
representation: "raw",
|
|
212
|
+
maxBytes: images.maxRequestImageBytes,
|
|
213
|
+
placeholder: (ref) => textOnlyImageText(ref)
|
|
214
|
+
});
|
|
215
|
+
const resolveImage = (block, signal) => imagePart(block, images.attachments, signal);
|
|
216
|
+
const system = options.system === void 0 ? [] : [{
|
|
217
|
+
role: "system",
|
|
218
|
+
content: options.system
|
|
219
|
+
}];
|
|
220
|
+
const prompt = await serializePrompt(requestMessages, resolveImage, images.signal);
|
|
221
|
+
return callOptionsWithPrompt(options, profile, model, [...system, ...prompt]);
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/translate.ts
|
|
225
|
+
function mapFinishReason(reason) {
|
|
226
|
+
switch (reason.unified) {
|
|
227
|
+
case "stop": return { kind: "stop" };
|
|
228
|
+
case "tool-calls": return { kind: "tool-calls" };
|
|
229
|
+
case "length": return { kind: "max-tokens" };
|
|
230
|
+
default: return {
|
|
231
|
+
kind: "error",
|
|
232
|
+
failure: {
|
|
233
|
+
message: `model stopped: ${reason.raw ?? reason.unified}`,
|
|
234
|
+
code: (reason.raw ?? reason.unified).toUpperCase()
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function mapUsage(usage) {
|
|
240
|
+
const cacheRead = usage.inputTokens.cacheRead;
|
|
241
|
+
const reasoning = usage.outputTokens.reasoning;
|
|
242
|
+
return {
|
|
243
|
+
inputTokens: usage.inputTokens.noCache ?? usage.inputTokens.total ?? 0,
|
|
244
|
+
outputTokens: usage.outputTokens.total ?? 0,
|
|
245
|
+
...cacheRead !== void 0 && cacheRead > 0 ? { cacheReadTokens: cacheRead } : {},
|
|
246
|
+
...reasoning !== void 0 && reasoning > 0 ? { reasoningTokens: reasoning } : {}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function closeBlock(block) {
|
|
250
|
+
switch (block.kind) {
|
|
251
|
+
case "text": return {
|
|
252
|
+
type: "text",
|
|
253
|
+
text: block.text
|
|
254
|
+
};
|
|
255
|
+
case "reasoning": return {
|
|
256
|
+
type: "reasoning",
|
|
257
|
+
text: block.text
|
|
258
|
+
};
|
|
259
|
+
case "tool-call": return {
|
|
260
|
+
type: "tool-call",
|
|
261
|
+
id: ToolCallId(block.callId ?? ""),
|
|
262
|
+
name: block.name ?? "",
|
|
263
|
+
arguments: block.text
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function* translate(stream) {
|
|
268
|
+
let nextIndex = 0;
|
|
269
|
+
const textBlocks = /* @__PURE__ */ new Map();
|
|
270
|
+
const reasoningBlocks = /* @__PURE__ */ new Map();
|
|
271
|
+
const toolBlocks = /* @__PURE__ */ new Map();
|
|
272
|
+
const toolQueue = [];
|
|
273
|
+
const order = [];
|
|
274
|
+
let pendingUsage;
|
|
275
|
+
let pendingFinish;
|
|
276
|
+
const open = (kind) => {
|
|
277
|
+
const block = {
|
|
278
|
+
index: nextIndex++,
|
|
279
|
+
kind,
|
|
280
|
+
text: ""
|
|
281
|
+
};
|
|
282
|
+
order.push(block);
|
|
283
|
+
return block;
|
|
284
|
+
};
|
|
285
|
+
for await (const part of stream) switch (part.type) {
|
|
286
|
+
case "stream-start":
|
|
287
|
+
case "response-metadata":
|
|
288
|
+
case "raw": break;
|
|
289
|
+
case "text-start": {
|
|
290
|
+
const block = open("text");
|
|
291
|
+
textBlocks.set(part.id, block);
|
|
292
|
+
yield {
|
|
293
|
+
type: "block-start",
|
|
294
|
+
index: block.index,
|
|
295
|
+
blockType: "text"
|
|
296
|
+
};
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case "text-delta": {
|
|
300
|
+
const block = textBlocks.get(part.id);
|
|
301
|
+
if (block === void 0) break;
|
|
302
|
+
block.text += part.delta;
|
|
303
|
+
yield {
|
|
304
|
+
type: "text-delta",
|
|
305
|
+
index: block.index,
|
|
306
|
+
text: part.delta
|
|
307
|
+
};
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case "text-end": break;
|
|
311
|
+
case "reasoning-start": {
|
|
312
|
+
const block = open("reasoning");
|
|
313
|
+
reasoningBlocks.set(part.id, block);
|
|
314
|
+
yield {
|
|
315
|
+
type: "block-start",
|
|
316
|
+
index: block.index,
|
|
317
|
+
blockType: "reasoning"
|
|
318
|
+
};
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case "reasoning-delta": {
|
|
322
|
+
const block = reasoningBlocks.get(part.id);
|
|
323
|
+
if (block === void 0) break;
|
|
324
|
+
block.text += part.delta;
|
|
325
|
+
yield {
|
|
326
|
+
type: "reasoning-delta",
|
|
327
|
+
index: block.index,
|
|
328
|
+
text: part.delta
|
|
329
|
+
};
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case "reasoning-end": break;
|
|
333
|
+
case "tool-input-start": {
|
|
334
|
+
const block = open("tool-call");
|
|
335
|
+
if (part.toolName !== void 0) block.name = part.toolName;
|
|
336
|
+
toolBlocks.set(part.id, block);
|
|
337
|
+
toolQueue.push(block);
|
|
338
|
+
yield {
|
|
339
|
+
type: "block-start",
|
|
340
|
+
index: block.index,
|
|
341
|
+
blockType: "tool-call"
|
|
342
|
+
};
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case "tool-input-delta": {
|
|
346
|
+
const block = toolBlocks.get(part.id);
|
|
347
|
+
if (block === void 0) break;
|
|
348
|
+
block.text += part.delta;
|
|
349
|
+
yield {
|
|
350
|
+
type: "tool-call-delta",
|
|
351
|
+
index: block.index,
|
|
352
|
+
id: ToolCallId(block.callId ?? part.id),
|
|
353
|
+
...block.name !== void 0 ? { name: block.name } : {},
|
|
354
|
+
argumentsDelta: part.delta
|
|
355
|
+
};
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case "tool-input-end": break;
|
|
359
|
+
case "tool-call":
|
|
360
|
+
applyToolCall(part, toolQueue);
|
|
361
|
+
break;
|
|
362
|
+
case "tool-result":
|
|
363
|
+
case "tool-approval-request":
|
|
364
|
+
case "custom":
|
|
365
|
+
case "file":
|
|
366
|
+
case "reasoning-file":
|
|
367
|
+
case "source": break;
|
|
368
|
+
case "finish":
|
|
369
|
+
pendingUsage = mapUsage(part.usage);
|
|
370
|
+
pendingFinish = mapFinishReason(part.finishReason);
|
|
371
|
+
for (const block of order) yield {
|
|
372
|
+
type: "block-end",
|
|
373
|
+
index: block.index,
|
|
374
|
+
block: closeBlock(block)
|
|
375
|
+
};
|
|
376
|
+
if (pendingUsage !== void 0) yield {
|
|
377
|
+
type: "usage",
|
|
378
|
+
usage: pendingUsage
|
|
379
|
+
};
|
|
380
|
+
const reason = pendingFinish ?? { kind: "stop" };
|
|
381
|
+
yield {
|
|
382
|
+
type: "finish",
|
|
383
|
+
reason: reason.kind === "stop" && order.length === 0 ? {
|
|
384
|
+
kind: "error",
|
|
385
|
+
failure: {
|
|
386
|
+
message: "model returned a completed response with no content",
|
|
387
|
+
code: EMPTY_RESPONSE_CODE
|
|
388
|
+
}
|
|
389
|
+
} : reason
|
|
390
|
+
};
|
|
391
|
+
return;
|
|
392
|
+
case "error": {
|
|
393
|
+
const error = part.error;
|
|
394
|
+
const cause = error instanceof Error ? error : void 0;
|
|
395
|
+
const message = cause?.message ?? (typeof error === "string" ? error : "provider stream error");
|
|
396
|
+
throw new LlmError(`OpenAI-compatible stream failed: ${message}`, "TRANSPORT", { cause });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
throw new LlmError("AI SDK stream ended without a finish part", "STREAM_CLOSED");
|
|
400
|
+
}
|
|
401
|
+
function applyToolCall(part, toolQueue) {
|
|
402
|
+
const block = toolQueue.shift();
|
|
403
|
+
if (block === void 0) return;
|
|
404
|
+
block.callId = part.toolCallId;
|
|
405
|
+
block.name = part.toolName;
|
|
406
|
+
block.text = part.input;
|
|
407
|
+
}
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/adapter.ts
|
|
410
|
+
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
411
|
+
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
412
|
+
const DEFAULT_MAX_TOKENS = 32768;
|
|
413
|
+
const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20971520;
|
|
414
|
+
const STREAM_IDLE_TIMEOUT_CODE = "LLM_STREAM_IDLE_TIMEOUT";
|
|
415
|
+
const REQUEST_TIMEOUT_CODE = "LLM_REQUEST_TIMEOUT";
|
|
416
|
+
const PROVIDER_OPTIONS_KEY = "openai-compatible";
|
|
417
|
+
function convertUsage(usage) {
|
|
418
|
+
if (usage == null) return {
|
|
419
|
+
inputTokens: {
|
|
420
|
+
total: 0,
|
|
421
|
+
noCache: 0,
|
|
422
|
+
cacheRead: void 0,
|
|
423
|
+
cacheWrite: void 0
|
|
424
|
+
},
|
|
425
|
+
outputTokens: {
|
|
426
|
+
total: 0,
|
|
427
|
+
text: void 0,
|
|
428
|
+
reasoning: void 0
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const promptTokens = usage.prompt_tokens ?? 0;
|
|
432
|
+
const completionTokens = usage.completion_tokens ?? 0;
|
|
433
|
+
const cacheRead = usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens ?? 0;
|
|
434
|
+
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? 0;
|
|
435
|
+
return {
|
|
436
|
+
inputTokens: {
|
|
437
|
+
total: promptTokens,
|
|
438
|
+
noCache: Math.max(0, promptTokens - cacheRead),
|
|
439
|
+
cacheRead,
|
|
440
|
+
cacheWrite: void 0
|
|
441
|
+
},
|
|
442
|
+
outputTokens: {
|
|
443
|
+
total: completionTokens,
|
|
444
|
+
text: Math.max(0, completionTokens - reasoningTokens),
|
|
445
|
+
reasoning: reasoningTokens
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function modelInfo(profile, model) {
|
|
450
|
+
return {
|
|
451
|
+
provider: profile.provider,
|
|
452
|
+
id: model.id,
|
|
453
|
+
name: model.name ?? model.id,
|
|
454
|
+
...model.description === void 0 ? {} : { description: model.description },
|
|
455
|
+
inputModalities: [...model.inputModalities]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function reasoningInfo(model, defaultEffort) {
|
|
459
|
+
const declaration = model?.reasoningEfforts;
|
|
460
|
+
if (declaration === void 0 || declaration === false) return {};
|
|
461
|
+
return { reasoning: {
|
|
462
|
+
efforts: Object.entries(declaration).map(([id]) => ({
|
|
463
|
+
id: ReasoningEffortId(id),
|
|
464
|
+
name: `${id.charAt(0).toUpperCase()}${id.slice(1)}`
|
|
465
|
+
})),
|
|
466
|
+
...defaultEffort !== void 0 && declaration[defaultEffort] !== void 0 ? { defaultEffort: ReasoningEffortId(defaultEffort) } : {}
|
|
467
|
+
} };
|
|
468
|
+
}
|
|
469
|
+
function httpErrorCode(status, error) {
|
|
470
|
+
if (status === 401 || status === 403) return "AUTH";
|
|
471
|
+
if (status === 413) return "INVALID_REQUEST";
|
|
472
|
+
const detail = [
|
|
473
|
+
error?.code,
|
|
474
|
+
error?.type,
|
|
475
|
+
error?.message
|
|
476
|
+
].filter(Boolean).join(" ");
|
|
477
|
+
if (isQuotaExceededError(detail)) return QUOTA_EXCEEDED_CODE;
|
|
478
|
+
if (status === 429) return "RATE_LIMIT";
|
|
479
|
+
if (status === 400) {
|
|
480
|
+
if (isContextWindowExceededError(detail)) return CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
481
|
+
return "INVALID_REQUEST";
|
|
482
|
+
}
|
|
483
|
+
if (status >= 500) return "SERVER";
|
|
484
|
+
return `HTTP_${status}`;
|
|
485
|
+
}
|
|
486
|
+
function providerErrorBody(error) {
|
|
487
|
+
if (error.responseBody === void 0) return void 0;
|
|
488
|
+
try {
|
|
489
|
+
return JSON.parse(error.responseBody).error;
|
|
490
|
+
} catch {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function requestId(headers) {
|
|
495
|
+
if (headers === void 0) return void 0;
|
|
496
|
+
const value = headers["x-request-id"] ?? headers["x-openai-compatible-request-id"];
|
|
497
|
+
return value === void 0 || value.length === 0 ? void 0 : ProviderRequestId(value);
|
|
498
|
+
}
|
|
499
|
+
var OpenAICompatibleAdapter = class extends LlmAdapter {
|
|
500
|
+
config;
|
|
501
|
+
sdkProviders = /* @__PURE__ */ new Map();
|
|
502
|
+
constructor(config) {
|
|
503
|
+
super();
|
|
504
|
+
this.config = config;
|
|
505
|
+
}
|
|
506
|
+
profileOf(provider) {
|
|
507
|
+
const profile = this.config.profiles().get(provider);
|
|
508
|
+
if (profile === void 0) throw new LlmError(`OpenAI-compatible adapter does not own provider "${provider}"`, "NO_ADAPTER");
|
|
509
|
+
return profile;
|
|
510
|
+
}
|
|
511
|
+
modelOf(profile, model) {
|
|
512
|
+
return profile.models.find((entry) => entry.id === model);
|
|
513
|
+
}
|
|
514
|
+
sdkModel(profile, modelId) {
|
|
515
|
+
let byModel = this.sdkProviders.get(profile);
|
|
516
|
+
if (byModel === void 0) {
|
|
517
|
+
byModel = /* @__PURE__ */ new Map();
|
|
518
|
+
this.sdkProviders.set(profile, byModel);
|
|
519
|
+
}
|
|
520
|
+
let model = byModel.get(modelId);
|
|
521
|
+
if (model === void 0) {
|
|
522
|
+
model = createOpenAICompatible({
|
|
523
|
+
name: PROVIDER_OPTIONS_KEY,
|
|
524
|
+
baseURL: profile.baseURL,
|
|
525
|
+
headers: {
|
|
526
|
+
...profile.headers,
|
|
527
|
+
...attributionHeaders()
|
|
528
|
+
},
|
|
529
|
+
includeUsage: true,
|
|
530
|
+
convertUsage
|
|
531
|
+
}).chatModel(modelId);
|
|
532
|
+
byModel.set(modelId, model);
|
|
533
|
+
}
|
|
534
|
+
return model;
|
|
535
|
+
}
|
|
536
|
+
providerInfo(provider) {
|
|
537
|
+
return {
|
|
538
|
+
id: provider,
|
|
539
|
+
name: this.config.profiles().get(provider)?.displayName ?? provider
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
providerRetryPolicy(provider) {
|
|
543
|
+
return this.config.profiles().get(provider)?.retryPolicy;
|
|
544
|
+
}
|
|
545
|
+
listModels(provider) {
|
|
546
|
+
const profile = this.profileOf(provider);
|
|
547
|
+
return Promise.resolve(profile.models.map((model) => modelInfo(profile, model)));
|
|
548
|
+
}
|
|
549
|
+
resolveModel(provider, model, _signal) {
|
|
550
|
+
const profile = this.profileOf(provider);
|
|
551
|
+
const configured = this.modelOf(profile, model);
|
|
552
|
+
const contextWindow = configured?.contextWindow ?? profile.defaultContextWindow;
|
|
553
|
+
const maxTokens = configured?.maxTokens ?? profile.defaultMaxTokens;
|
|
554
|
+
return Promise.resolve({
|
|
555
|
+
...configured === void 0 ? {
|
|
556
|
+
provider,
|
|
557
|
+
id: model,
|
|
558
|
+
name: model,
|
|
559
|
+
inputModalities: ["text"]
|
|
560
|
+
} : modelInfo(profile, configured),
|
|
561
|
+
context: { contextWindow },
|
|
562
|
+
...maxTokens !== void 0 ? { defaultMaxTokens: maxTokens } : {},
|
|
563
|
+
...reasoningInfo(configured, profile.reasoning)
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
async *stream(options) {
|
|
567
|
+
const profile = this.profileOf(options.provider);
|
|
568
|
+
const model = this.modelOf(profile, options.model);
|
|
569
|
+
const hasImages = options.messages.some((message) => contentHasImage(message.content));
|
|
570
|
+
let attachments;
|
|
571
|
+
if (hasImages) {
|
|
572
|
+
if (model?.inputModalities.includes("image") !== true) throw new LlmError(`OpenAI-compatible model "${options.model}" does not accept image input.`, "UNSUPPORTED_CONTENT");
|
|
573
|
+
attachments = this.config.resolveAttachments?.();
|
|
574
|
+
if (attachments === void 0) throw new LlmError("OpenAI-compatible image conversion requires the durable attachment service.", "UNSUPPORTED_CONTENT");
|
|
575
|
+
}
|
|
576
|
+
const apiKey = await this.config.resolveApiKey(options.provider, profile);
|
|
577
|
+
const userId = this.config.resolveUserId();
|
|
578
|
+
const consumer = new AbortController();
|
|
579
|
+
const upstream = options.signal === void 0 ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]);
|
|
580
|
+
const overall = profile.timeoutMs === void 0 ? void 0 : deadline(upstream, profile.timeoutMs, REQUEST_TIMEOUT_CODE);
|
|
581
|
+
const watchdog = idleWatchdog(overall?.signal ?? upstream, profile.streamIdleTimeoutMs, STREAM_IDLE_TIMEOUT_CODE);
|
|
582
|
+
try {
|
|
583
|
+
const callOptions = attachments === void 0 ? await serializeCallOptions(options, profile, model) : await serializeCallOptionsWithImages(options, profile, model, {
|
|
584
|
+
attachments,
|
|
585
|
+
maxRequestImageBytes: profile.maxRequestImageBytes,
|
|
586
|
+
signal: watchdog.signal
|
|
587
|
+
});
|
|
588
|
+
const sdkModel = this.sdkModel(profile, options.model);
|
|
589
|
+
let result;
|
|
590
|
+
try {
|
|
591
|
+
result = await sdkModel.doStream({
|
|
592
|
+
...callOptions,
|
|
593
|
+
abortSignal: watchdog.signal,
|
|
594
|
+
headers: {
|
|
595
|
+
...apiKey === void 0 ? {} : { authorization: `Bearer ${apiKey}` },
|
|
596
|
+
"x-openai-compatible-harness-user-id": String(userId),
|
|
597
|
+
...options.sessionId !== void 0 ? { "x-openai-compatible-harness-session-id": String(options.sessionId) } : {},
|
|
598
|
+
...options.purpose === "compaction" ? { "x-openai-compatible-harness-compact": "1" } : {}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
} catch (error) {
|
|
602
|
+
throw this.normalizeTransportError(error, profile);
|
|
603
|
+
}
|
|
604
|
+
const iterator = translate(result.stream)[Symbol.asyncIterator]();
|
|
605
|
+
let exhausted = false;
|
|
606
|
+
try {
|
|
607
|
+
while (true) {
|
|
608
|
+
const next = await watchdog.next(iterator);
|
|
609
|
+
if (next.done) {
|
|
610
|
+
exhausted = true;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
yield next.value;
|
|
614
|
+
}
|
|
615
|
+
} catch (error) {
|
|
616
|
+
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 });
|
|
617
|
+
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 });
|
|
618
|
+
if (options.signal?.aborted) throw new LlmError("OpenAI-compatible request aborted by caller", "ABORTED", { cause: error });
|
|
619
|
+
if (error instanceof LlmError) throw error;
|
|
620
|
+
throw this.normalizeTransportError(error, profile);
|
|
621
|
+
} finally {
|
|
622
|
+
consumer.abort("OpenAI-compatible stream consumer stopped");
|
|
623
|
+
if (!exhausted) try {
|
|
624
|
+
await iterator.return(void 0);
|
|
625
|
+
} catch {}
|
|
626
|
+
}
|
|
627
|
+
} finally {
|
|
628
|
+
watchdog[Symbol.dispose]();
|
|
629
|
+
overall?.[Symbol.dispose]();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
normalizeTransportError(error, profile) {
|
|
633
|
+
if (error instanceof LlmError) return error;
|
|
634
|
+
if (APICallError.isInstance(error)) {
|
|
635
|
+
const providerError = providerErrorBody(error);
|
|
636
|
+
const message = typeof providerError?.message === "string" ? providerError.message : error.message;
|
|
637
|
+
const id = requestId(error.responseHeaders);
|
|
638
|
+
return new LlmError(message, httpErrorCode(error.statusCode ?? 0, providerError), {
|
|
639
|
+
...error.statusCode === void 0 ? {} : { status: error.statusCode },
|
|
640
|
+
...id === void 0 ? {} : { requestId: id },
|
|
641
|
+
cause: error
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (error instanceof Error) return new LlmError(`OpenAI-compatible API request to ${profile.baseURL} failed`, "TRANSPORT", { cause: error });
|
|
645
|
+
return new LlmError(`OpenAI-compatible API request to ${profile.baseURL} failed`, "TRANSPORT");
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/index.ts
|
|
650
|
+
const name = "llm-openai-compatible";
|
|
651
|
+
const inject = ["llm"];
|
|
652
|
+
const NS = "llm-openai-compatible";
|
|
653
|
+
const REASONING_LEVELS = [
|
|
654
|
+
"off",
|
|
655
|
+
"low",
|
|
656
|
+
"high",
|
|
657
|
+
"max"
|
|
658
|
+
];
|
|
659
|
+
const MODEL_MODALITIES = ["text", "image"];
|
|
660
|
+
const modelSchema = z.object({
|
|
661
|
+
id: z.string().required(),
|
|
662
|
+
name: z.string(),
|
|
663
|
+
description: z.string(),
|
|
664
|
+
contextWindow: z.number().step(1).min(1),
|
|
665
|
+
maxTokens: z.number().step(1).min(1),
|
|
666
|
+
inputModalities: z.array(z.union(MODEL_MODALITIES)).min(1).default(["text"]),
|
|
667
|
+
reasoningEfforts: z.union([z.const(false), z.dict(z.union([z.string(), z.const(null)]))])
|
|
668
|
+
});
|
|
669
|
+
const providerSchema = z.object({
|
|
670
|
+
apiKeyEnv: z.string().role("credential-ref"),
|
|
671
|
+
displayName: z.string(),
|
|
672
|
+
baseURL: z.string().required(),
|
|
673
|
+
headers: z.dict(z.string()),
|
|
674
|
+
temperature: z.number().min(0).max(2),
|
|
675
|
+
topP: z.number().min(0).max(1),
|
|
676
|
+
topK: z.number().step(1).min(1),
|
|
677
|
+
presencePenalty: z.number().min(-2).max(2),
|
|
678
|
+
frequencyPenalty: z.number().min(-2).max(2),
|
|
679
|
+
seed: z.number().step(1).min(1),
|
|
680
|
+
reasoning: z.union(REASONING_LEVELS),
|
|
681
|
+
models: z.array(modelSchema),
|
|
682
|
+
defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
|
|
683
|
+
defaultMaxTokens: z.number().step(1).min(1).default(DEFAULT_MAX_TOKENS),
|
|
684
|
+
maxRequestImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES),
|
|
685
|
+
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
686
|
+
timeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS),
|
|
687
|
+
retryPolicy: RetryPolicySchema
|
|
688
|
+
});
|
|
689
|
+
const Config = z.object({ providers: z.dict(providerSchema).default({}) });
|
|
690
|
+
function isReasoningEffort(value) {
|
|
691
|
+
return REASONING_LEVELS.includes(value);
|
|
692
|
+
}
|
|
693
|
+
function resolveReasoningEfforts(provider, modelId, value) {
|
|
694
|
+
if (value === void 0) return {};
|
|
695
|
+
if (value === false) return { reasoningEfforts: false };
|
|
696
|
+
const declaration = {};
|
|
697
|
+
for (const [effort, wire] of Object.entries(value)) {
|
|
698
|
+
if (!isReasoningEffort(effort)) throw new Error(`llm-openai-compatible: provider "${provider}" model "${modelId}" declares unknown reasoning effort "${effort}"`);
|
|
699
|
+
if (effort === "off") {
|
|
700
|
+
if (wire !== null) throw new Error(`llm-openai-compatible: provider "${provider}" model "${modelId}" reasoning effort "off" must leave an empty wire spelling (null) to omit reasoning_effort`);
|
|
701
|
+
declaration.off = null;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (wire === null || wire.length === 0) throw new Error(`llm-openai-compatible: provider "${provider}" model "${modelId}" reasoning effort "${effort}" needs a non-empty wire spelling`);
|
|
705
|
+
declaration[effort] = wire;
|
|
706
|
+
}
|
|
707
|
+
return { reasoningEfforts: declaration };
|
|
708
|
+
}
|
|
709
|
+
function resolveModels(provider, models) {
|
|
710
|
+
if (models === void 0) return [];
|
|
711
|
+
const seen = /* @__PURE__ */ new Set();
|
|
712
|
+
return models.map((model) => {
|
|
713
|
+
if (model.id.length === 0) throw new Error(`llm-openai-compatible: provider "${provider}" catalog model ids must be non-empty`);
|
|
714
|
+
if (model.name !== void 0 && model.name.length === 0) throw new Error(`llm-openai-compatible: provider "${provider}" catalog model "${model.id}" has an empty name`);
|
|
715
|
+
if (model.contextWindow !== void 0 && (!Number.isInteger(model.contextWindow) || model.contextWindow <= 0)) throw new Error(`llm-openai-compatible: provider "${provider}" catalog model "${model.id}" contextWindow must be a positive integer`);
|
|
716
|
+
if (model.maxTokens !== void 0 && (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0)) throw new Error(`llm-openai-compatible: provider "${provider}" catalog model "${model.id}" maxTokens must be a positive integer`);
|
|
717
|
+
const inputModalities = model.inputModalities ?? ["text"];
|
|
718
|
+
if (inputModalities.length === 0) throw new Error(`llm-openai-compatible: provider "${provider}" catalog model "${model.id}" inputModalities must not be empty`);
|
|
719
|
+
if (inputModalities.some((modality) => !MODEL_MODALITIES.includes(modality))) throw new Error(`llm-openai-compatible: provider "${provider}" catalog model "${model.id}" inputModalities must contain only "text" and "image"`);
|
|
720
|
+
if (new Set(inputModalities).size !== inputModalities.length) throw new Error(`llm-openai-compatible: provider "${provider}" catalog model "${model.id}" inputModalities must not contain duplicates`);
|
|
721
|
+
if (seen.has(model.id)) throw new Error(`llm-openai-compatible: provider "${provider}" has duplicate catalog model "${model.id}"`);
|
|
722
|
+
seen.add(model.id);
|
|
723
|
+
return {
|
|
724
|
+
id: model.id,
|
|
725
|
+
...model.name === void 0 ? {} : { name: model.name },
|
|
726
|
+
...model.description === void 0 ? {} : { description: model.description },
|
|
727
|
+
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
728
|
+
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
|
|
729
|
+
inputModalities: [...inputModalities],
|
|
730
|
+
...resolveReasoningEfforts(provider, model.id, model.reasoningEfforts)
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function bounded(value, lo, hi) {
|
|
735
|
+
if (value === void 0) return void 0;
|
|
736
|
+
if (!Number.isFinite(value) || value < lo || value > hi) return void 0;
|
|
737
|
+
return value;
|
|
738
|
+
}
|
|
739
|
+
function resolveAdapterOptions(provider, source) {
|
|
740
|
+
if (provider.length === 0) throw new Error("llm-openai-compatible: provider names must be non-empty");
|
|
741
|
+
if (source.baseURL === void 0 || source.baseURL.length === 0) throw new Error(`llm-openai-compatible: provider "${provider}" requires a non-empty baseURL`);
|
|
742
|
+
if (source.displayName !== void 0 && source.displayName.length === 0) throw new Error(`llm-openai-compatible: provider "${provider}" has an empty displayName`);
|
|
743
|
+
const streamIdleTimeoutMs = source.streamIdleTimeoutMs ?? 3e5;
|
|
744
|
+
if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) throw new Error(`llm-openai-compatible: provider "${provider}" streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
745
|
+
const maxRequestImageBytes = source.maxRequestImageBytes ?? 20971520;
|
|
746
|
+
if (!Number.isSafeInteger(maxRequestImageBytes) || maxRequestImageBytes <= 0) throw new Error(`llm-openai-compatible: provider "${provider}" maxRequestImageBytes must be a positive safe integer`);
|
|
747
|
+
const defaultContextWindow = source.defaultContextWindow ?? 262144;
|
|
748
|
+
if (!Number.isInteger(defaultContextWindow) || defaultContextWindow <= 0) throw new Error(`llm-openai-compatible: provider "${provider}" defaultContextWindow must be a positive integer`);
|
|
749
|
+
const defaultMaxTokens = source.defaultMaxTokens ?? 32768;
|
|
750
|
+
if (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0) throw new Error(`llm-openai-compatible: provider "${provider}" defaultMaxTokens must be a positive safe integer`);
|
|
751
|
+
const timeoutMs = bounded(source.timeoutMs, Number.MIN_VALUE, MAX_TIMER_DELAY_MS);
|
|
752
|
+
if (source.timeoutMs !== void 0 && timeoutMs === void 0) throw new Error(`llm-openai-compatible: provider "${provider}" timeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
753
|
+
if (bounded(source.temperature, 0, 2) === void 0 && source.temperature !== void 0) throw new Error(`llm-openai-compatible: provider "${provider}" temperature must be a finite number within 0..2`);
|
|
754
|
+
if (bounded(source.topP, 0, 1) === void 0 && source.topP !== void 0) throw new Error(`llm-openai-compatible: provider "${provider}" topP must be a finite number within 0..1`);
|
|
755
|
+
if (source.topK !== void 0 && (!Number.isInteger(source.topK) || source.topK <= 0)) throw new Error(`llm-openai-compatible: provider "${provider}" topK must be a positive integer`);
|
|
756
|
+
if (bounded(source.presencePenalty, -2, 2) === void 0 && source.presencePenalty !== void 0) throw new Error(`llm-openai-compatible: provider "${provider}" presencePenalty must be a finite number within -2..2`);
|
|
757
|
+
if (bounded(source.frequencyPenalty, -2, 2) === void 0 && source.frequencyPenalty !== void 0) throw new Error(`llm-openai-compatible: provider "${provider}" frequencyPenalty must be a finite number within -2..2`);
|
|
758
|
+
if (source.seed !== void 0 && (!Number.isInteger(source.seed) || source.seed <= 0)) throw new Error(`llm-openai-compatible: provider "${provider}" seed must be a positive integer`);
|
|
759
|
+
if (source.reasoning !== void 0 && !isReasoningEffort(source.reasoning)) throw new Error(`llm-openai-compatible: provider "${provider}" reasoning must be one of ${REASONING_LEVELS.join(", ")}`);
|
|
760
|
+
return {
|
|
761
|
+
provider,
|
|
762
|
+
displayName: source.displayName ?? provider,
|
|
763
|
+
...source.apiKeyEnv === void 0 ? {} : { apiKeyEnv: credentialRef(source.apiKeyEnv) },
|
|
764
|
+
baseURL: source.baseURL,
|
|
765
|
+
...source.headers === void 0 ? {} : { headers: { ...source.headers } },
|
|
766
|
+
...source.temperature === void 0 ? {} : { temperature: source.temperature },
|
|
767
|
+
...source.topP === void 0 ? {} : { topP: source.topP },
|
|
768
|
+
...source.topK === void 0 ? {} : { topK: source.topK },
|
|
769
|
+
...source.presencePenalty === void 0 ? {} : { presencePenalty: source.presencePenalty },
|
|
770
|
+
...source.frequencyPenalty === void 0 ? {} : { frequencyPenalty: source.frequencyPenalty },
|
|
771
|
+
...source.seed === void 0 ? {} : { seed: source.seed },
|
|
772
|
+
...source.reasoning === void 0 ? {} : { reasoning: source.reasoning },
|
|
773
|
+
models: resolveModels(provider, source.models),
|
|
774
|
+
defaultContextWindow,
|
|
775
|
+
defaultMaxTokens,
|
|
776
|
+
maxRequestImageBytes,
|
|
777
|
+
streamIdleTimeoutMs,
|
|
778
|
+
...timeoutMs === void 0 ? {} : { timeoutMs },
|
|
779
|
+
retryPolicy: resolveRetryPolicy(source.retryPolicy, `llm-openai-compatible: provider "${provider}" retryPolicy`)
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function resolveProfiles(providers) {
|
|
783
|
+
if (Array.isArray(providers)) throw new Error("llm-openai-compatible: providers is now a dict keyed by provider route, not an array of profiles");
|
|
784
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
785
|
+
for (const [provider, source] of Object.entries(providers ?? {})) resolved.set(provider, resolveAdapterOptions(provider, source));
|
|
786
|
+
return resolved;
|
|
787
|
+
}
|
|
788
|
+
function assertServiceable(config) {
|
|
789
|
+
resolveProfiles(config.providers);
|
|
790
|
+
}
|
|
791
|
+
function registrationFacts(profiles) {
|
|
792
|
+
return [...profiles.entries()].map(([provider, profile]) => ({
|
|
793
|
+
provider,
|
|
794
|
+
displayName: profile.displayName,
|
|
795
|
+
retryPolicy: profile.retryPolicy
|
|
796
|
+
})).sort((left, right) => left.provider.localeCompare(right.provider));
|
|
797
|
+
}
|
|
798
|
+
function directoryEntries(profiles) {
|
|
799
|
+
const entries = /* @__PURE__ */ new Map();
|
|
800
|
+
for (const [provider, profile] of profiles) entries.set(provider, {
|
|
801
|
+
provider,
|
|
802
|
+
displayName: profile.displayName,
|
|
803
|
+
settingsNs: NS,
|
|
804
|
+
settingsPath: ["providers", provider],
|
|
805
|
+
declared: true
|
|
806
|
+
});
|
|
807
|
+
return [...entries.values()];
|
|
808
|
+
}
|
|
809
|
+
function apply(ctx, config) {
|
|
810
|
+
let current = () => config;
|
|
811
|
+
let lastRaw;
|
|
812
|
+
let memoized;
|
|
813
|
+
const profiles = () => {
|
|
814
|
+
const raw = current();
|
|
815
|
+
if (raw === lastRaw && memoized !== void 0) return memoized;
|
|
816
|
+
const next = resolveProfiles(raw.providers);
|
|
817
|
+
lastRaw = raw;
|
|
818
|
+
memoized = next;
|
|
819
|
+
return next;
|
|
820
|
+
};
|
|
821
|
+
profiles();
|
|
822
|
+
const resolveApiKey = async (provider, profile) => {
|
|
823
|
+
const ref = profile.apiKeyEnv;
|
|
824
|
+
if (ref === void 0) return void 0;
|
|
825
|
+
const credentials = ctx.get("credentials");
|
|
826
|
+
if (credentials !== void 0) {
|
|
827
|
+
const hit = await credentials.resolve(ref);
|
|
828
|
+
if (hit !== void 0) return assertUsableApiKey(hit.value, "llm-openai-compatible", ref);
|
|
829
|
+
} else {
|
|
830
|
+
const ambient = launchEnvironmentOf(ctx).get(ref);
|
|
831
|
+
if (ambient !== void 0 && ambient.value.length > 0) return assertUsableApiKey(ambient.value, "llm-openai-compatible", ref);
|
|
832
|
+
}
|
|
833
|
+
throw new LlmError(`llm-openai-compatible: no credential for provider route "${provider}"; its profile resolves ${ref}, which is not set — store ${ref} through the credentials service (the web Models page writes it), or export ${ref} in the launching environment`, "MISSING_CREDENTIAL");
|
|
834
|
+
};
|
|
835
|
+
let userId;
|
|
836
|
+
const resolveUserId = () => userId ??= getOrCreateAnonymousUserId();
|
|
837
|
+
const adapter = new OpenAICompatibleAdapter({
|
|
838
|
+
profiles,
|
|
839
|
+
resolveApiKey,
|
|
840
|
+
resolveUserId,
|
|
841
|
+
resolveAttachments: () => ctx.get("attachments")
|
|
842
|
+
});
|
|
843
|
+
let directory;
|
|
844
|
+
let directoryFacts;
|
|
845
|
+
const ensureDirectory = () => {
|
|
846
|
+
const entries = directoryEntries(profiles());
|
|
847
|
+
if (deepEqualJson(entries, directoryFacts)) return;
|
|
848
|
+
if (directory === void 0) directory = ctx.llm.registerConfigurableProviders(entries);
|
|
849
|
+
else directory.replace(entries);
|
|
850
|
+
directoryFacts = entries;
|
|
851
|
+
};
|
|
852
|
+
ensureDirectory();
|
|
853
|
+
let registration;
|
|
854
|
+
let registeredFacts;
|
|
855
|
+
const ensureRegistrationFacts = () => {
|
|
856
|
+
const facts = registrationFacts(profiles());
|
|
857
|
+
if (deepEqualJson(facts, registeredFacts)) return;
|
|
858
|
+
const routes = [...profiles().keys()];
|
|
859
|
+
if (registration === void 0) {
|
|
860
|
+
if (routes.length === 0) {
|
|
861
|
+
registeredFacts = facts;
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
registration = ctx.llm.registerAdapter(routes, adapter);
|
|
865
|
+
} else registration.replace(routes);
|
|
866
|
+
registeredFacts = facts;
|
|
867
|
+
};
|
|
868
|
+
ensureRegistrationFacts();
|
|
869
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
870
|
+
settingsCtx.settings.installSection(ctx, NS, Config, config, {
|
|
871
|
+
validate: assertServiceable,
|
|
872
|
+
setSource: (source) => {
|
|
873
|
+
current = source;
|
|
874
|
+
},
|
|
875
|
+
onChange: () => {
|
|
876
|
+
try {
|
|
877
|
+
ensureRegistrationFacts();
|
|
878
|
+
} catch (error) {
|
|
879
|
+
ctx.logger.error("llm-openai-compatible: keeping the previously registered routes after a refused update");
|
|
880
|
+
ctx.logger.error(error);
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
ensureDirectory();
|
|
884
|
+
} catch (error) {
|
|
885
|
+
ctx.logger.error("llm-openai-compatible: keeping the previous configurable-provider directory after a refused update");
|
|
886
|
+
ctx.logger.error(error);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
//#endregion
|
|
893
|
+
export { Config, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, MODEL_MODALITIES, NS, OpenAICompatibleAdapter, REASONING_LEVELS, apply, assertServiceable, inject, name, resolveAdapterOptions, resolveProfiles };
|
|
894
|
+
|
|
895
|
+
//# sourceMappingURL=index.mjs.map
|