@eliotstocker/open-harness-plugin-openai-compatible-auto 0.0.0-development
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 +63 -0
- package/cordis.patch.yml +3 -0
- package/dist/core/auth.d.ts +56 -0
- package/dist/core/auth.js +407 -0
- package/dist/core/discovery.d.ts +41 -0
- package/dist/core/discovery.js +222 -0
- package/dist/dsh.d.ts +30 -0
- package/dist/dsh.js +583 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +376 -0
- package/dist/pi.d.ts +8 -0
- package/dist/pi.js +272 -0
- package/package.json +121 -0
package/dist/dsh.js
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
2
|
+
import { credentialKey } from "@deepseek-ai/dsh-credentials";
|
|
3
|
+
import { attributionHeaders, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import "@deepseek-ai/dsh-authorization";
|
|
5
|
+
import "@deepseek-ai/dsh-settings";
|
|
6
|
+
import z from "@deepseek-ai/schemastery";
|
|
7
|
+
import { isContextOverflow, } from "@earendil-works/pi-ai";
|
|
8
|
+
import * as piAiNs from "@earendil-works/pi-ai";
|
|
9
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
10
|
+
import { NEVER_EXPIRES, refreshLiteLLMTokens, runLiteLLMLogin } from "./core/auth.js";
|
|
11
|
+
import { fetchDiscoveredModels, fetchGatewayUserInfo, rootURL, v1URL, } from "./core/discovery.js";
|
|
12
|
+
export const name = "openai-auto";
|
|
13
|
+
export const inject = ["llm"];
|
|
14
|
+
const PROVIDER_ID = "openai-auto";
|
|
15
|
+
const RECORD_KEY = credentialKey(name, PROVIDER_ID);
|
|
16
|
+
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
17
|
+
const DEFAULT_MAX_TOKENS = 16_384;
|
|
18
|
+
const REFRESH_TTL_MS = 5 * 60 * 1000;
|
|
19
|
+
const EXPIRY_SKEW_MS = 60_000;
|
|
20
|
+
const APP_HEADERS = { "X-Prov-Application": "dsh" };
|
|
21
|
+
export const Config = z.object({
|
|
22
|
+
displayName: z.string().description("Display name shown in model pickers"),
|
|
23
|
+
baseURL: z.string().description("OpenAI-compatible endpoint URL (optional if signed in via /login)"),
|
|
24
|
+
apiKeyEnv: z.string().description("Credential/environment variable name for API key"),
|
|
25
|
+
headers: z.dict(z.string()).description("Extra HTTP headers sent on requests"),
|
|
26
|
+
defaultContextWindow: z.number().default(DEFAULT_CONTEXT_WINDOW),
|
|
27
|
+
defaultMaxTokens: z.number().default(DEFAULT_MAX_TOKENS),
|
|
28
|
+
});
|
|
29
|
+
function asRecord(v) {
|
|
30
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
31
|
+
}
|
|
32
|
+
function readStr(v) {
|
|
33
|
+
return typeof v === "string" && v.trim().length > 0 ? v.trim() : undefined;
|
|
34
|
+
}
|
|
35
|
+
function parseGrant(v) {
|
|
36
|
+
const rec = asRecord(v);
|
|
37
|
+
const access = readStr(rec.access);
|
|
38
|
+
const baseURL = readStr(rec.baseURL);
|
|
39
|
+
if (!access || !baseURL)
|
|
40
|
+
return undefined;
|
|
41
|
+
return {
|
|
42
|
+
access,
|
|
43
|
+
refresh: typeof rec.refresh === "string" ? rec.refresh : "",
|
|
44
|
+
expires: typeof rec.expires === "number" && Number.isFinite(rec.expires) ? rec.expires : NEVER_EXPIRES,
|
|
45
|
+
baseURL: rootURL(baseURL),
|
|
46
|
+
metadata: asRecord(rec.metadata),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function zeroUsage() {
|
|
50
|
+
return {
|
|
51
|
+
input: 0,
|
|
52
|
+
output: 0,
|
|
53
|
+
cacheRead: 0,
|
|
54
|
+
cacheWrite: 0,
|
|
55
|
+
totalTokens: 0,
|
|
56
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function flattenText(message) {
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const block of message.content) {
|
|
62
|
+
if (block.type === "text")
|
|
63
|
+
out.push(block.text);
|
|
64
|
+
else if (block.type === "file")
|
|
65
|
+
out.push(`[file: ${block.attachment.name}]`);
|
|
66
|
+
}
|
|
67
|
+
return out.join("");
|
|
68
|
+
}
|
|
69
|
+
function toolResultText(blocks) {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const block of blocks) {
|
|
72
|
+
if (block.type === "text")
|
|
73
|
+
out.push(block.text);
|
|
74
|
+
else if (block.type === "file")
|
|
75
|
+
out.push(`[file: ${block.attachment.name}]`);
|
|
76
|
+
else if (block.type === "image")
|
|
77
|
+
out.push(`[image: ${block.attachment.mediaType}]`);
|
|
78
|
+
}
|
|
79
|
+
return out.join("\n");
|
|
80
|
+
}
|
|
81
|
+
function parseArgs(raw) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
function toPiContext(options) {
|
|
92
|
+
let systemPrompt = options.system;
|
|
93
|
+
let rest = options.messages;
|
|
94
|
+
if (systemPrompt === undefined && rest[0]?.role === "system") {
|
|
95
|
+
const text = flattenText(rest[0]);
|
|
96
|
+
systemPrompt = text.length > 0 ? text : undefined;
|
|
97
|
+
rest = rest.slice(1);
|
|
98
|
+
}
|
|
99
|
+
const toolNames = new Map();
|
|
100
|
+
const messages = [];
|
|
101
|
+
for (const msg of rest) {
|
|
102
|
+
if (msg.role === "system") {
|
|
103
|
+
messages.push({ role: "user", content: flattenText(msg), timestamp: 0 });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (msg.role === "assistant") {
|
|
107
|
+
const content = [];
|
|
108
|
+
for (const block of msg.content) {
|
|
109
|
+
if (block.type === "text")
|
|
110
|
+
content.push({ type: "text", text: block.text });
|
|
111
|
+
else if (block.type === "reasoning")
|
|
112
|
+
content.push({ type: "thinking", thinking: block.text });
|
|
113
|
+
else if (block.type === "tool-call") {
|
|
114
|
+
toolNames.set(String(block.id), block.name);
|
|
115
|
+
content.push({
|
|
116
|
+
type: "toolCall",
|
|
117
|
+
id: String(block.id),
|
|
118
|
+
name: block.name,
|
|
119
|
+
arguments: parseArgs(block.arguments),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
messages.push({
|
|
124
|
+
role: "assistant",
|
|
125
|
+
content,
|
|
126
|
+
api: "openai-completions",
|
|
127
|
+
provider: PROVIDER_ID,
|
|
128
|
+
model: options.model,
|
|
129
|
+
usage: zeroUsage(),
|
|
130
|
+
stopReason: content.some((b) => b.type === "toolCall") ? "toolUse" : "stop",
|
|
131
|
+
timestamp: 0,
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const text = flattenText(msg);
|
|
136
|
+
const results = msg.content.filter((b) => b.type === "tool-result");
|
|
137
|
+
if (text.length > 0 || results.length === 0) {
|
|
138
|
+
messages.push({ role: "user", content: text, timestamp: 0 });
|
|
139
|
+
}
|
|
140
|
+
for (const r of results) {
|
|
141
|
+
const tr = {
|
|
142
|
+
role: "toolResult",
|
|
143
|
+
toolCallId: String(r.toolCallId),
|
|
144
|
+
toolName: toolNames.get(String(r.toolCallId)) ?? "unknown",
|
|
145
|
+
content: [{ type: "text", text: toolResultText(r.content) || "(no output)" }],
|
|
146
|
+
isError: r.isError ?? false,
|
|
147
|
+
timestamp: 0,
|
|
148
|
+
};
|
|
149
|
+
messages.push(tr);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
...(systemPrompt !== undefined ? { systemPrompt } : {}),
|
|
154
|
+
messages,
|
|
155
|
+
...(options.tools && options.tools.length > 0
|
|
156
|
+
? {
|
|
157
|
+
tools: options.tools.map((t) => ({
|
|
158
|
+
name: t.name,
|
|
159
|
+
description: t.description,
|
|
160
|
+
parameters: t.parameters,
|
|
161
|
+
})),
|
|
162
|
+
}
|
|
163
|
+
: {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function mapUsage(u) {
|
|
167
|
+
return {
|
|
168
|
+
inputTokens: u.input,
|
|
169
|
+
outputTokens: u.output,
|
|
170
|
+
totalTokens: u.totalTokens,
|
|
171
|
+
...(u.cacheRead > 0 ? { cacheReadTokens: u.cacheRead } : {}),
|
|
172
|
+
...(u.cacheWrite > 0 ? { cacheWriteTokens: u.cacheWrite } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function classifyError(message) {
|
|
176
|
+
if (/\b(?:401|403)\b/.test(message))
|
|
177
|
+
return "AUTH";
|
|
178
|
+
if (isQuotaExceededError(message))
|
|
179
|
+
return QUOTA_EXCEEDED_CODE;
|
|
180
|
+
if (/\b429\b|rate.?limit/i.test(message))
|
|
181
|
+
return "RATE_LIMIT";
|
|
182
|
+
if (/\b400\b|\b413\b|invalid.?request|payload too large/i.test(message))
|
|
183
|
+
return "INVALID_REQUEST";
|
|
184
|
+
if (/\b5\d\d\b/.test(message))
|
|
185
|
+
return "SERVER";
|
|
186
|
+
if (/\btime(?:d)?\s*out\b|timeout/i.test(message))
|
|
187
|
+
return "TIMEOUT";
|
|
188
|
+
if (/\b(?:network|connection|socket|fetch)\b|\bECONN[A-Z]+\b/i.test(message))
|
|
189
|
+
return "TRANSPORT";
|
|
190
|
+
return "OPENAI_AUTO_ERROR";
|
|
191
|
+
}
|
|
192
|
+
function mapStopReason(msg, contextWindow) {
|
|
193
|
+
if (isContextOverflow(msg, contextWindow) ||
|
|
194
|
+
(msg.stopReason === "error" && msg.errorMessage && isContextWindowExceededError(msg.errorMessage))) {
|
|
195
|
+
return {
|
|
196
|
+
kind: "error",
|
|
197
|
+
failure: {
|
|
198
|
+
message: msg.errorMessage ?? `Context window exceeded for "${msg.model}"`,
|
|
199
|
+
code: CONTEXT_WINDOW_EXCEEDED_CODE,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
switch (msg.stopReason) {
|
|
204
|
+
case "stop":
|
|
205
|
+
if (msg.content.length === 0) {
|
|
206
|
+
return {
|
|
207
|
+
kind: "error",
|
|
208
|
+
failure: {
|
|
209
|
+
message: `Model "${msg.model}" returned an empty response`,
|
|
210
|
+
code: EMPTY_RESPONSE_CODE,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return { kind: "stop" };
|
|
215
|
+
case "length":
|
|
216
|
+
return { kind: "max-tokens" };
|
|
217
|
+
case "toolUse":
|
|
218
|
+
return { kind: "tool-calls" };
|
|
219
|
+
case "aborted":
|
|
220
|
+
return {
|
|
221
|
+
kind: "aborted",
|
|
222
|
+
failure: { message: msg.errorMessage ?? "Stream aborted", code: "ABORTED" },
|
|
223
|
+
};
|
|
224
|
+
default: {
|
|
225
|
+
const text = msg.errorMessage ?? "OpenAI-compatible stream error";
|
|
226
|
+
return { kind: "error", failure: { message: text, code: classifyError(text) } };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function* toStreamChunks(events, contextWindow, callerSignal) {
|
|
231
|
+
const toolIds = new Map();
|
|
232
|
+
for await (const event of events) {
|
|
233
|
+
switch (event.type) {
|
|
234
|
+
case "text_start":
|
|
235
|
+
yield { type: "block-start", index: event.contentIndex, blockType: "text" };
|
|
236
|
+
break;
|
|
237
|
+
case "text_delta":
|
|
238
|
+
yield { type: "text-delta", index: event.contentIndex, text: event.delta };
|
|
239
|
+
break;
|
|
240
|
+
case "text_end":
|
|
241
|
+
yield { type: "block-end", index: event.contentIndex, block: { type: "text", text: event.content } };
|
|
242
|
+
break;
|
|
243
|
+
case "thinking_start":
|
|
244
|
+
yield { type: "block-start", index: event.contentIndex, blockType: "reasoning" };
|
|
245
|
+
break;
|
|
246
|
+
case "thinking_delta":
|
|
247
|
+
yield { type: "reasoning-delta", index: event.contentIndex, text: event.delta };
|
|
248
|
+
break;
|
|
249
|
+
case "thinking_end":
|
|
250
|
+
yield { type: "block-end", index: event.contentIndex, block: { type: "reasoning", text: event.content } };
|
|
251
|
+
break;
|
|
252
|
+
case "toolcall_start": {
|
|
253
|
+
const partial = event.partial.content[event.contentIndex];
|
|
254
|
+
const id = partial?.type === "toolCall" ? partial.id : "";
|
|
255
|
+
const toolName = partial?.type === "toolCall" ? partial.name : "";
|
|
256
|
+
toolIds.set(event.contentIndex, { id, name: toolName });
|
|
257
|
+
yield { type: "block-start", index: event.contentIndex, blockType: "tool-call" };
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "toolcall_delta": {
|
|
261
|
+
const known = toolIds.get(event.contentIndex);
|
|
262
|
+
yield {
|
|
263
|
+
type: "tool-call-delta",
|
|
264
|
+
index: event.contentIndex,
|
|
265
|
+
id: brandString(known?.id ?? ""),
|
|
266
|
+
...(known?.name ? { name: known.name } : {}),
|
|
267
|
+
argumentsDelta: event.delta,
|
|
268
|
+
};
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "toolcall_end":
|
|
272
|
+
yield {
|
|
273
|
+
type: "block-end",
|
|
274
|
+
index: event.contentIndex,
|
|
275
|
+
block: {
|
|
276
|
+
type: "tool-call",
|
|
277
|
+
id: brandString(event.toolCall.id),
|
|
278
|
+
name: event.toolCall.name,
|
|
279
|
+
arguments: JSON.stringify(event.toolCall.arguments),
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
break;
|
|
283
|
+
case "done":
|
|
284
|
+
yield { type: "usage", usage: mapUsage(event.message.usage) };
|
|
285
|
+
yield { type: "finish", reason: mapStopReason(event.message, contextWindow) };
|
|
286
|
+
return;
|
|
287
|
+
case "error":
|
|
288
|
+
yield { type: "usage", usage: mapUsage(event.error.usage) };
|
|
289
|
+
yield {
|
|
290
|
+
type: "finish",
|
|
291
|
+
reason: mapStopReason(callerSignal?.aborted ? { ...event.error, stopReason: "aborted" } : event.error, contextWindow),
|
|
292
|
+
};
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
throw new LlmError("OpenAI-compatible stream ended without terminal event", "STREAM_CLOSED");
|
|
297
|
+
}
|
|
298
|
+
function mapReasoningEfforts(efforts) {
|
|
299
|
+
if (!efforts || efforts.length === 0)
|
|
300
|
+
return [];
|
|
301
|
+
return efforts.map((id) => ({
|
|
302
|
+
id: ReasoningEffortId(id),
|
|
303
|
+
name: id.charAt(0).toUpperCase() + id.slice(1),
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
function toPiReasoning(effort) {
|
|
307
|
+
if (!effort)
|
|
308
|
+
return undefined;
|
|
309
|
+
const lower = effort.toLowerCase();
|
|
310
|
+
if (lower === "minimal" || lower === "low" || lower === "medium" || lower === "high" || lower === "xhigh") {
|
|
311
|
+
return lower;
|
|
312
|
+
}
|
|
313
|
+
if (lower === "max")
|
|
314
|
+
return "xhigh";
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
export function apply(ctx, initialConfig = {}) {
|
|
318
|
+
let getConfig = () => initialConfig;
|
|
319
|
+
let cachedModels = [];
|
|
320
|
+
let cachedUser;
|
|
321
|
+
let cachedAt = 0;
|
|
322
|
+
let cachedKey = "";
|
|
323
|
+
const requestHeaders = (extra) => ({
|
|
324
|
+
...APP_HEADERS,
|
|
325
|
+
...extra,
|
|
326
|
+
...attributionHeaders(),
|
|
327
|
+
});
|
|
328
|
+
const resolveEndpointAndAuth = async (signal) => {
|
|
329
|
+
const cfg = getConfig();
|
|
330
|
+
const credentials = ctx.get("credentials");
|
|
331
|
+
if (credentials) {
|
|
332
|
+
const rec = await credentials.readRecord(RECORD_KEY).catch(() => undefined);
|
|
333
|
+
if (rec?.kind === "grant") {
|
|
334
|
+
const grant = parseGrant(rec.payload);
|
|
335
|
+
if (grant) {
|
|
336
|
+
if (Date.now() + EXPIRY_SKEW_MS >= grant.expires) {
|
|
337
|
+
const refreshed = await credentials.modifyRecord(RECORD_KEY, async (current) => {
|
|
338
|
+
if (current?.kind !== "grant")
|
|
339
|
+
return current;
|
|
340
|
+
const cur = parseGrant(current.payload);
|
|
341
|
+
if (!cur)
|
|
342
|
+
return current;
|
|
343
|
+
if (Date.now() + EXPIRY_SKEW_MS < cur.expires)
|
|
344
|
+
return current;
|
|
345
|
+
const tok = await refreshLiteLLMTokens(cur, requestHeaders(cfg.headers), signal);
|
|
346
|
+
return { kind: "grant", payload: { ...cur, ...tok } };
|
|
347
|
+
});
|
|
348
|
+
const updated = refreshed?.kind === "grant" ? parseGrant(refreshed.payload) : undefined;
|
|
349
|
+
if (updated)
|
|
350
|
+
return { baseURL: updated.baseURL, apiKey: updated.access };
|
|
351
|
+
}
|
|
352
|
+
return { baseURL: grant.baseURL, apiKey: grant.access };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const baseURL = readStr(cfg.baseURL) ?? readStr(process.env.OPENAI_AUTO_BASE_URL) ?? readStr(process.env.LITELLM_BASE_URL);
|
|
357
|
+
if (!baseURL) {
|
|
358
|
+
throw new LlmError("openai-auto is not configured. Run /login openai-auto or set baseURL in settings.", "MISSING_CREDENTIAL");
|
|
359
|
+
}
|
|
360
|
+
let apiKey;
|
|
361
|
+
const envRef = readStr(cfg.apiKeyEnv);
|
|
362
|
+
if (envRef && credentials) {
|
|
363
|
+
apiKey = (await credentials.resolve(envRef).catch(() => undefined))?.value;
|
|
364
|
+
}
|
|
365
|
+
else if (envRef) {
|
|
366
|
+
apiKey = readStr(process.env[envRef]);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
apiKey = readStr(process.env.OPENAI_AUTO_API_KEY) ?? readStr(process.env.LITELLM_API_KEY);
|
|
370
|
+
}
|
|
371
|
+
return { baseURL: rootURL(baseURL), apiKey };
|
|
372
|
+
};
|
|
373
|
+
const refreshModels = async (force = false, signal) => {
|
|
374
|
+
let auth;
|
|
375
|
+
try {
|
|
376
|
+
auth = await resolveEndpointAndAuth(signal);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return cachedModels;
|
|
380
|
+
}
|
|
381
|
+
const cacheKey = `${auth.baseURL}:${auth.apiKey ?? ""}`;
|
|
382
|
+
if (!force && cachedKey === cacheKey && Date.now() - cachedAt < REFRESH_TTL_MS && cachedModels.length > 0) {
|
|
383
|
+
return cachedModels;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const hdrs = requestHeaders(getConfig().headers);
|
|
387
|
+
const [models, userInfo] = await Promise.all([
|
|
388
|
+
fetchDiscoveredModels(auth.baseURL, auth.apiKey, hdrs, signal),
|
|
389
|
+
fetchGatewayUserInfo(auth.baseURL, auth.apiKey, hdrs, signal),
|
|
390
|
+
]);
|
|
391
|
+
cachedModels = models;
|
|
392
|
+
cachedUser = userInfo;
|
|
393
|
+
cachedAt = Date.now();
|
|
394
|
+
cachedKey = cacheKey;
|
|
395
|
+
return models;
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
ctx.logger.warn(`openai-auto: /v1/models discovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
399
|
+
return cachedModels;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const formatDisplayName = () => {
|
|
403
|
+
const base = readStr(getConfig().displayName) ?? "OpenAI-Compatible (Auto)";
|
|
404
|
+
if (!cachedUser?.email)
|
|
405
|
+
return base;
|
|
406
|
+
const spend = cachedUser.effectiveSpendUsd !== undefined && cachedUser.monthlyCapUsd !== undefined
|
|
407
|
+
? ` ($${cachedUser.effectiveSpendUsd.toFixed(2)}/$${cachedUser.monthlyCapUsd.toFixed(0)})`
|
|
408
|
+
: "";
|
|
409
|
+
return `${base} · ${cachedUser.email}${spend}`;
|
|
410
|
+
};
|
|
411
|
+
class OpenAIAutoDshAdapter extends LlmAdapter {
|
|
412
|
+
providerInfo(provider) {
|
|
413
|
+
return {
|
|
414
|
+
id: provider,
|
|
415
|
+
name: formatDisplayName(),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async listModels(provider) {
|
|
419
|
+
const list = await refreshModels(false);
|
|
420
|
+
return list.map((m) => ({
|
|
421
|
+
provider,
|
|
422
|
+
id: m.id,
|
|
423
|
+
name: m.name,
|
|
424
|
+
inputModalities: m.supportsVision ? ["text", "image"] : ["text"],
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
async resolveModel(provider, model, signal) {
|
|
428
|
+
const list = await refreshModels(false, signal);
|
|
429
|
+
const found = list.find((m) => m.id === model);
|
|
430
|
+
const cfg = getConfig();
|
|
431
|
+
const efforts = found?.reasoning ? mapReasoningEfforts(found.reasoningEfforts) : [];
|
|
432
|
+
return {
|
|
433
|
+
provider,
|
|
434
|
+
id: model,
|
|
435
|
+
name: found?.name ?? model,
|
|
436
|
+
inputModalities: found?.supportsVision ? ["text", "image"] : ["text"],
|
|
437
|
+
context: { contextWindow: found?.contextWindow ?? cfg.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW },
|
|
438
|
+
defaultMaxTokens: found?.maxTokens ?? cfg.defaultMaxTokens ?? DEFAULT_MAX_TOKENS,
|
|
439
|
+
...(efforts.length > 0 ? { reasoning: { efforts } } : {}),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
async *stream(options) {
|
|
443
|
+
const cfg = getConfig();
|
|
444
|
+
const { baseURL, apiKey } = await resolveEndpointAndAuth(options.signal);
|
|
445
|
+
const resolved = await this.resolveModel(options.provider, options.model, options.signal);
|
|
446
|
+
const meta = cachedModels.find((m) => m.id === options.model);
|
|
447
|
+
const contextWindow = resolved.context?.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
448
|
+
const reasoning = toPiReasoning(options.reasoningEffort ? String(options.reasoningEffort) : undefined);
|
|
449
|
+
const piModel = {
|
|
450
|
+
id: options.model,
|
|
451
|
+
name: resolved.name,
|
|
452
|
+
api: "openai-completions",
|
|
453
|
+
provider: PROVIDER_ID,
|
|
454
|
+
baseUrl: v1URL(baseURL),
|
|
455
|
+
reasoning: meta?.reasoning ?? Boolean(reasoning),
|
|
456
|
+
input: resolved.inputModalities?.includes("image") ? ["text", "image"] : ["text"],
|
|
457
|
+
cost: meta?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
458
|
+
contextWindow,
|
|
459
|
+
maxTokens: resolved.defaultMaxTokens ?? DEFAULT_MAX_TOKENS,
|
|
460
|
+
compat: {
|
|
461
|
+
supportsStore: false,
|
|
462
|
+
supportsUsageInStreaming: true,
|
|
463
|
+
supportsReasoningEffort: Boolean(reasoning),
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
const rawPiCtx = toPiContext(options);
|
|
467
|
+
const normalizeContext = piAiNs.normalizeContext;
|
|
468
|
+
const piCtx = typeof normalizeContext === "function"
|
|
469
|
+
? normalizeContext(rawPiCtx)
|
|
470
|
+
: rawPiCtx;
|
|
471
|
+
const piEvents = openAICompletionsApi().streamSimple(piModel, piCtx, {
|
|
472
|
+
...(apiKey ? { apiKey } : {}),
|
|
473
|
+
...(reasoning ? { reasoning } : {}),
|
|
474
|
+
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
475
|
+
...(options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : {}),
|
|
476
|
+
...(options.sessionId !== undefined ? { sessionId: String(options.sessionId) } : {}),
|
|
477
|
+
signal: options.signal,
|
|
478
|
+
headers: requestHeaders(cfg.headers),
|
|
479
|
+
});
|
|
480
|
+
yield* toStreamChunks(piEvents, contextWindow, options.signal);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const adapter = new OpenAIAutoDshAdapter();
|
|
484
|
+
ctx.llm.registerAdapter([PROVIDER_ID], adapter);
|
|
485
|
+
ctx.llm.registerConfigurableProviders([
|
|
486
|
+
{
|
|
487
|
+
provider: PROVIDER_ID,
|
|
488
|
+
displayName: formatDisplayName(),
|
|
489
|
+
settingsNs: name,
|
|
490
|
+
settingsPath: [],
|
|
491
|
+
declared: true,
|
|
492
|
+
},
|
|
493
|
+
]);
|
|
494
|
+
ctx.llm.registerModelDiscovery(name, async (req, signal) => {
|
|
495
|
+
const auth = req.baseURL
|
|
496
|
+
? { baseURL: req.baseURL, apiKey: req.apiKey }
|
|
497
|
+
: await resolveEndpointAndAuth(signal);
|
|
498
|
+
const list = await fetchDiscoveredModels(auth.baseURL, auth.apiKey, requestHeaders(getConfig().headers), signal);
|
|
499
|
+
return list.map((m) => ({
|
|
500
|
+
id: m.id,
|
|
501
|
+
name: m.name,
|
|
502
|
+
...(m.contextWindow !== undefined ? { contextWindow: m.contextWindow } : {}),
|
|
503
|
+
...(m.maxTokens !== undefined ? { maxTokens: m.maxTokens } : {}),
|
|
504
|
+
}));
|
|
505
|
+
});
|
|
506
|
+
ctx.inject(["authorization", "credentials"], (authCtx) => {
|
|
507
|
+
authCtx.authorization.registerFlow({
|
|
508
|
+
key: RECORD_KEY,
|
|
509
|
+
label: "OpenAI-Compatible (Auto)",
|
|
510
|
+
methods: [
|
|
511
|
+
{ id: "litellm-cli", label: "LiteLLM CLI Code Flow (Browser)" },
|
|
512
|
+
{ id: "api-key", label: "API Key + Base URL" },
|
|
513
|
+
],
|
|
514
|
+
async run(session) {
|
|
515
|
+
const defaultUrl = readStr(getConfig().baseURL) ?? readStr(process.env.LITELLM_BASE_URL) ?? "https://llm.example.com";
|
|
516
|
+
const rawUrl = await session.prompt({
|
|
517
|
+
kind: "text",
|
|
518
|
+
message: "Endpoint Base URL",
|
|
519
|
+
placeholder: defaultUrl,
|
|
520
|
+
});
|
|
521
|
+
const baseURL = readStr(rawUrl) ?? readStr(getConfig().baseURL) ?? readStr(process.env.LITELLM_BASE_URL);
|
|
522
|
+
if (!baseURL)
|
|
523
|
+
throw new LlmError("Base URL is required", "INVALID_ARGS");
|
|
524
|
+
if (session.method === "api-key") {
|
|
525
|
+
const apiKey = readStr(await session.prompt({
|
|
526
|
+
kind: "secret",
|
|
527
|
+
message: "API key (optional for local endpoints)",
|
|
528
|
+
placeholder: "sk-...",
|
|
529
|
+
}));
|
|
530
|
+
await fetchDiscoveredModels(baseURL, apiKey, requestHeaders(getConfig().headers), session.signal);
|
|
531
|
+
await authCtx.credentials.modifyRecord(RECORD_KEY, async () => ({
|
|
532
|
+
kind: "grant",
|
|
533
|
+
payload: {
|
|
534
|
+
access: apiKey ?? "local",
|
|
535
|
+
refresh: "",
|
|
536
|
+
expires: NEVER_EXPIRES,
|
|
537
|
+
baseURL: rootURL(baseURL),
|
|
538
|
+
},
|
|
539
|
+
}));
|
|
540
|
+
void refreshModels(true).catch(() => { });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const team = readStr(await session.prompt({
|
|
544
|
+
kind: "text",
|
|
545
|
+
message: "LiteLLM team (optional)",
|
|
546
|
+
placeholder: "Leave blank if not required",
|
|
547
|
+
}));
|
|
548
|
+
const tok = await runLiteLLMLogin(baseURL, { providerID: PROVIDER_ID, team, headers: requestHeaders(getConfig().headers), clientName: "dsh" }, {
|
|
549
|
+
signal: session.signal,
|
|
550
|
+
onAuth: (url, instructions) => {
|
|
551
|
+
session.notify({ message: instructions, url });
|
|
552
|
+
},
|
|
553
|
+
onPrompt: (message, placeholder) => session.prompt({ kind: "secret", message, placeholder }),
|
|
554
|
+
});
|
|
555
|
+
await authCtx.credentials.modifyRecord(RECORD_KEY, async () => ({
|
|
556
|
+
kind: "grant",
|
|
557
|
+
payload: {
|
|
558
|
+
access: tok.access,
|
|
559
|
+
refresh: tok.refresh,
|
|
560
|
+
expires: tok.expires,
|
|
561
|
+
baseURL: rootURL(baseURL),
|
|
562
|
+
metadata: tok.metadata,
|
|
563
|
+
},
|
|
564
|
+
}));
|
|
565
|
+
void refreshModels(true).catch(() => { });
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
ctx.on("credentials/record-updated", (key) => {
|
|
570
|
+
if (key === RECORD_KEY)
|
|
571
|
+
void refreshModels(true).catch(() => { });
|
|
572
|
+
});
|
|
573
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
574
|
+
settingsCtx.settings.installSection(ctx, name, Config, initialConfig, {
|
|
575
|
+
setSource: (source) => {
|
|
576
|
+
getConfig = source;
|
|
577
|
+
},
|
|
578
|
+
onChange: () => {
|
|
579
|
+
void refreshModels(true).catch(() => { });
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
}
|
package/dist/index.d.ts
ADDED