@d-ai/omp 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -2
- package/dist/index.js +1253 -0
- package/package.json +34 -4
package/README.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @d-ai/omp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
omp 插件。登录 D.AI 后,`/model` 里使用 `dai/<文本模型>`。只接对话模型,没有生图、视频、音频。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
omp plugin install @d-ai/omp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
重启 omp。
|
|
12
|
+
|
|
13
|
+
从本仓库开发时:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm -C sdks/typescript build
|
|
17
|
+
cd sdks/omp && npm install && npm run build
|
|
18
|
+
omp plugin link /绝对路径/sdks/omp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 发布
|
|
22
|
+
|
|
23
|
+
改 `package.json` 的 `version`(npm 会拒绝已经发过的版本),在 `sdks/omp` 执行 `npm publish --access public`。
|
|
24
|
+
|
|
25
|
+
## 登录
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
omp login dai
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
1. `1`:浏览器授权。生产走 `https://dai.itbug.shop`,`DAI_BASE_URL` 指向 `127.0.0.1` 或 `localhost` 时走本地。
|
|
32
|
+
2. `2`:粘贴 `ddsk_` API key。
|
|
33
|
+
|
|
34
|
+
模型列表跟这次登录的站点。OAuth 登录后,没再设置 `DAI_BASE_URL` 也会用凭证里的 issuer,而不是改去生产。`/v1/models` 目前只认 `ddsk_`;OAuth 令牌被拒时改用该站点的公开文本目录。
|
|
35
|
+
|
|
36
|
+
也可以不登录,直接 `export DAI_API_KEY=ddsk_...`。
|
|
37
|
+
|
|
38
|
+
然后 `/model`,选 `dai/` 开头的模型。Claude 的 `@300k`、`@1m`、`:fast`、`:slow` 与 Pi 插件相同。
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1253 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
//#region ../pi/src/claude-variants.ts
|
|
6
|
+
const CONTEXT_300K = 3e5;
|
|
7
|
+
const CONTEXT_1M = 1e6;
|
|
8
|
+
/** Gateway accepts `speed: fast` only for these upstream ids. */
|
|
9
|
+
const FAST_CLAUDE_MODELS = /* @__PURE__ */ new Set(["claude-opus-4-8", "claude-opus-5"]);
|
|
10
|
+
const CLAUDE_VARIANT_ID = /^(claude-[a-z0-9]+(?:[._-][a-z0-9]+)*)@(300k|1m)(?::(fast|slow))?$/iu;
|
|
11
|
+
function expandClaudeChatModels(models) {
|
|
12
|
+
const expanded = [];
|
|
13
|
+
for (const model of models) {
|
|
14
|
+
expanded.push(model);
|
|
15
|
+
if (model.api !== "anthropic-messages") continue;
|
|
16
|
+
const speeds = FAST_CLAUDE_MODELS.has(model.id) ? [
|
|
17
|
+
void 0,
|
|
18
|
+
"fast",
|
|
19
|
+
"slow"
|
|
20
|
+
] : [void 0];
|
|
21
|
+
for (const context of contextChoices(model.contextWindow)) for (const speed of speeds) expanded.push(variantModel(model, context, speed));
|
|
22
|
+
}
|
|
23
|
+
return expanded;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pi sends `model.id` as the Anthropic `model` field. Variant rows keep a
|
|
27
|
+
* suffixed id so compaction uses 300k or 1m, and this rewrite restores the
|
|
28
|
+
* upstream id plus `speed` before the gateway sees the body.
|
|
29
|
+
* Return `undefined` to leave the payload unchanged.
|
|
30
|
+
*/
|
|
31
|
+
function rewriteClaudeProviderPayload(payload) {
|
|
32
|
+
if (!isRecord(payload) || typeof payload.model !== "string") return;
|
|
33
|
+
const parsed = parseClaudeVariantId(payload.model);
|
|
34
|
+
if (!parsed) return;
|
|
35
|
+
const next = {
|
|
36
|
+
...payload,
|
|
37
|
+
model: parsed.upstreamId
|
|
38
|
+
};
|
|
39
|
+
if (parsed.speed === "fast" && FAST_CLAUDE_MODELS.has(parsed.upstreamId)) next.speed = "fast";
|
|
40
|
+
else if (parsed.speed === "slow") next.speed = "standard";
|
|
41
|
+
else delete next.speed;
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
function contextChoices(contextWindow) {
|
|
45
|
+
const choices = [];
|
|
46
|
+
if (contextWindow > CONTEXT_300K) choices.push({
|
|
47
|
+
label: "300k",
|
|
48
|
+
tokens: CONTEXT_300K
|
|
49
|
+
});
|
|
50
|
+
if (contextWindow >= CONTEXT_1M) choices.push({
|
|
51
|
+
label: "1m",
|
|
52
|
+
tokens: CONTEXT_1M
|
|
53
|
+
});
|
|
54
|
+
return choices;
|
|
55
|
+
}
|
|
56
|
+
function variantModel(model, context, speed) {
|
|
57
|
+
const suffix = speed === void 0 ? "" : `:${speed}`;
|
|
58
|
+
return {
|
|
59
|
+
...model,
|
|
60
|
+
id: `${model.id}@${context.label}${suffix}`,
|
|
61
|
+
name: `${model.name} @${context.label}${suffix}`,
|
|
62
|
+
contextWindow: context.tokens,
|
|
63
|
+
maxTokens: Math.min(model.maxTokens, context.tokens),
|
|
64
|
+
...speed === "fast" ? { cost: doubleCost(model.cost) } : {}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function doubleCost(cost) {
|
|
68
|
+
return {
|
|
69
|
+
input: cost.input * 2,
|
|
70
|
+
output: cost.output * 2,
|
|
71
|
+
cacheRead: cost.cacheRead * 2,
|
|
72
|
+
cacheWrite: cost.cacheWrite * 2
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function parseClaudeVariantId(id) {
|
|
76
|
+
const match = CLAUDE_VARIANT_ID.exec(id);
|
|
77
|
+
if (!match?.[1] || !match[2]) return;
|
|
78
|
+
const speed = match[3]?.toLowerCase();
|
|
79
|
+
return {
|
|
80
|
+
upstreamId: match[1].toLowerCase(),
|
|
81
|
+
...speed === "fast" || speed === "slow" ? { speed } : {}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function isRecord(value) {
|
|
85
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region ../pi/src/thinking.ts
|
|
89
|
+
/** GPT-5.6: off→none, no minimal, xhigh+max. */
|
|
90
|
+
const GPT_5_6 = {
|
|
91
|
+
off: "none",
|
|
92
|
+
minimal: null,
|
|
93
|
+
low: "low",
|
|
94
|
+
medium: "medium",
|
|
95
|
+
high: "high",
|
|
96
|
+
xhigh: "xhigh",
|
|
97
|
+
max: "max"
|
|
98
|
+
};
|
|
99
|
+
/** GPT-6: thinking cannot be disabled; low through max. */
|
|
100
|
+
const GPT_6 = {
|
|
101
|
+
off: null,
|
|
102
|
+
minimal: null,
|
|
103
|
+
low: "low",
|
|
104
|
+
medium: "medium",
|
|
105
|
+
high: "high",
|
|
106
|
+
xhigh: "xhigh",
|
|
107
|
+
max: "max"
|
|
108
|
+
};
|
|
109
|
+
/** GPT-5.2–5.5: off→none, no minimal, xhigh, no max. */
|
|
110
|
+
const GPT_5_4 = {
|
|
111
|
+
off: "none",
|
|
112
|
+
minimal: null,
|
|
113
|
+
low: "low",
|
|
114
|
+
medium: "medium",
|
|
115
|
+
high: "high",
|
|
116
|
+
xhigh: "xhigh",
|
|
117
|
+
max: null
|
|
118
|
+
};
|
|
119
|
+
const GPT_5_1 = {
|
|
120
|
+
off: "none",
|
|
121
|
+
minimal: null,
|
|
122
|
+
low: "low",
|
|
123
|
+
medium: "medium",
|
|
124
|
+
high: "high",
|
|
125
|
+
xhigh: null,
|
|
126
|
+
max: null
|
|
127
|
+
};
|
|
128
|
+
/** Original GPT-5: thinking cannot be off; minimal through high. */
|
|
129
|
+
const GPT_5 = {
|
|
130
|
+
off: null,
|
|
131
|
+
minimal: "minimal",
|
|
132
|
+
low: "low",
|
|
133
|
+
medium: "medium",
|
|
134
|
+
high: "high",
|
|
135
|
+
xhigh: null,
|
|
136
|
+
max: null
|
|
137
|
+
};
|
|
138
|
+
const O_SERIES = {
|
|
139
|
+
off: null,
|
|
140
|
+
minimal: null,
|
|
141
|
+
low: "low",
|
|
142
|
+
medium: "medium",
|
|
143
|
+
high: "high",
|
|
144
|
+
xhigh: null,
|
|
145
|
+
max: null
|
|
146
|
+
};
|
|
147
|
+
const KNOWN = {
|
|
148
|
+
"gpt-6-astra": GPT_6,
|
|
149
|
+
"gpt-5.6-luna": GPT_5_6,
|
|
150
|
+
"gpt-5.6-sol": GPT_5_6,
|
|
151
|
+
"gpt-5.6-terra": GPT_5_6,
|
|
152
|
+
"gpt-5.5": GPT_5_4,
|
|
153
|
+
"gpt-5.5-pro": {
|
|
154
|
+
...GPT_5_4,
|
|
155
|
+
off: null,
|
|
156
|
+
low: null
|
|
157
|
+
},
|
|
158
|
+
"gpt-5.4": GPT_5_4,
|
|
159
|
+
"gpt-5.4-mini": GPT_5_4,
|
|
160
|
+
"gpt-5.4-nano": GPT_5_4,
|
|
161
|
+
"gpt-5.4-pro": {
|
|
162
|
+
...GPT_5_4,
|
|
163
|
+
off: null,
|
|
164
|
+
low: null
|
|
165
|
+
},
|
|
166
|
+
"gpt-5.2": GPT_5_4,
|
|
167
|
+
"gpt-5.1": GPT_5_1,
|
|
168
|
+
"gpt-5": GPT_5,
|
|
169
|
+
"gpt-5-mini": GPT_5,
|
|
170
|
+
"gpt-5-nano": GPT_5,
|
|
171
|
+
"gpt-5-pro": {
|
|
172
|
+
off: null,
|
|
173
|
+
minimal: null,
|
|
174
|
+
low: null,
|
|
175
|
+
medium: null,
|
|
176
|
+
high: "high",
|
|
177
|
+
xhigh: null,
|
|
178
|
+
max: null
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const NO_REASONING = /* @__PURE__ */ new Set(["gpt-5-chat-latest", "gpt-5.3-chat-latest"]);
|
|
182
|
+
/** Opus 4.7+ / Fable: adaptive effort. off and minimal are unsupported. */
|
|
183
|
+
const CLAUDE_XHIGHER = {
|
|
184
|
+
xhigh: "xhigh",
|
|
185
|
+
max: "max"
|
|
186
|
+
};
|
|
187
|
+
const CLAUDE_OFF = {
|
|
188
|
+
off: null,
|
|
189
|
+
xhigh: "xhigh",
|
|
190
|
+
max: "max"
|
|
191
|
+
};
|
|
192
|
+
/** Opus 5.5 accepts the full effort range. */
|
|
193
|
+
const CLAUDE_FULL = {
|
|
194
|
+
off: null,
|
|
195
|
+
minimal: null,
|
|
196
|
+
low: "low",
|
|
197
|
+
medium: "medium",
|
|
198
|
+
high: "high",
|
|
199
|
+
xhigh: "xhigh",
|
|
200
|
+
max: "max"
|
|
201
|
+
};
|
|
202
|
+
const CLAUDE_ADAPTIVE = {
|
|
203
|
+
forceAdaptiveThinking: true,
|
|
204
|
+
supportsStrictTools: true
|
|
205
|
+
};
|
|
206
|
+
const CLAUDE_NO_TEMPERATURE = {
|
|
207
|
+
...CLAUDE_ADAPTIVE,
|
|
208
|
+
supportsTemperature: false
|
|
209
|
+
};
|
|
210
|
+
const CLAUDE_PROFILES = {
|
|
211
|
+
"claude-fable-5": {
|
|
212
|
+
reasoning: true,
|
|
213
|
+
thinkingLevelMap: CLAUDE_OFF,
|
|
214
|
+
input: ["text", "image"],
|
|
215
|
+
compat: CLAUDE_ADAPTIVE
|
|
216
|
+
},
|
|
217
|
+
"claude-opus-4-7": {
|
|
218
|
+
reasoning: true,
|
|
219
|
+
thinkingLevelMap: CLAUDE_XHIGHER,
|
|
220
|
+
input: ["text", "image"],
|
|
221
|
+
compat: CLAUDE_NO_TEMPERATURE
|
|
222
|
+
},
|
|
223
|
+
"claude-opus-4-8": {
|
|
224
|
+
reasoning: true,
|
|
225
|
+
thinkingLevelMap: CLAUDE_XHIGHER,
|
|
226
|
+
input: ["text", "image"],
|
|
227
|
+
compat: CLAUDE_NO_TEMPERATURE
|
|
228
|
+
},
|
|
229
|
+
"claude-opus-5": {
|
|
230
|
+
reasoning: true,
|
|
231
|
+
thinkingLevelMap: CLAUDE_OFF,
|
|
232
|
+
input: ["text", "image"],
|
|
233
|
+
compat: CLAUDE_NO_TEMPERATURE
|
|
234
|
+
},
|
|
235
|
+
"claude-opus-5-5": {
|
|
236
|
+
reasoning: true,
|
|
237
|
+
thinkingLevelMap: CLAUDE_FULL,
|
|
238
|
+
input: ["text", "image"],
|
|
239
|
+
compat: CLAUDE_NO_TEMPERATURE
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
function chatModelThinking(id) {
|
|
243
|
+
const claude = claudeThinking(id);
|
|
244
|
+
if (claude) return claude;
|
|
245
|
+
if (NO_REASONING.has(id)) return {
|
|
246
|
+
reasoning: false,
|
|
247
|
+
input: ["text", "image"]
|
|
248
|
+
};
|
|
249
|
+
const known = KNOWN[id];
|
|
250
|
+
if (known) return {
|
|
251
|
+
reasoning: true,
|
|
252
|
+
thinkingLevelMap: known,
|
|
253
|
+
input: visionInput(id)
|
|
254
|
+
};
|
|
255
|
+
if (/^gpt-6(?:[-.]|$)/iu.test(id)) return {
|
|
256
|
+
reasoning: true,
|
|
257
|
+
thinkingLevelMap: GPT_6,
|
|
258
|
+
input: ["text", "image"]
|
|
259
|
+
};
|
|
260
|
+
if (/^gpt-5\.6/iu.test(id)) return {
|
|
261
|
+
reasoning: true,
|
|
262
|
+
thinkingLevelMap: GPT_5_6,
|
|
263
|
+
input: ["text", "image"]
|
|
264
|
+
};
|
|
265
|
+
if (/^gpt-5\.(?:2|3|4|5)/iu.test(id)) return {
|
|
266
|
+
reasoning: true,
|
|
267
|
+
thinkingLevelMap: GPT_5_4,
|
|
268
|
+
input: ["text", "image"]
|
|
269
|
+
};
|
|
270
|
+
if (/^gpt-5\.1/iu.test(id)) return {
|
|
271
|
+
reasoning: true,
|
|
272
|
+
thinkingLevelMap: GPT_5_1,
|
|
273
|
+
input: ["text", "image"]
|
|
274
|
+
};
|
|
275
|
+
if (/^gpt-5(?:[-.]|$)/iu.test(id)) return {
|
|
276
|
+
reasoning: true,
|
|
277
|
+
thinkingLevelMap: GPT_5,
|
|
278
|
+
input: ["text", "image"]
|
|
279
|
+
};
|
|
280
|
+
if (/^(?:o1|o3|o4)(?:[-.]|$)/iu.test(id)) return {
|
|
281
|
+
reasoning: true,
|
|
282
|
+
thinkingLevelMap: O_SERIES,
|
|
283
|
+
input: ["text"]
|
|
284
|
+
};
|
|
285
|
+
if (/reason/iu.test(id)) return {
|
|
286
|
+
reasoning: true,
|
|
287
|
+
input: ["text"]
|
|
288
|
+
};
|
|
289
|
+
return {
|
|
290
|
+
reasoning: false,
|
|
291
|
+
input: ["text"]
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function visionInput(id) {
|
|
295
|
+
return /^(?:gpt-5|gpt-6)/iu.test(id) ? ["text", "image"] : ["text"];
|
|
296
|
+
}
|
|
297
|
+
function claudeThinking(id) {
|
|
298
|
+
const known = CLAUDE_PROFILES[id];
|
|
299
|
+
if (known) return known;
|
|
300
|
+
if (!/^claude-/iu.test(id)) return;
|
|
301
|
+
if (/^claude-opus-5-5(?:[-.]|$)/iu.test(id)) return CLAUDE_PROFILES["claude-opus-5-5"];
|
|
302
|
+
if (/^claude-opus-5(?:[-.]|$)/iu.test(id)) return CLAUDE_PROFILES["claude-opus-5"];
|
|
303
|
+
if (/^claude-opus-4-8(?:[-.]|$)/iu.test(id)) return CLAUDE_PROFILES["claude-opus-4-8"];
|
|
304
|
+
if (/^claude-opus-4-7(?:[-.]|$)/iu.test(id)) return CLAUDE_PROFILES["claude-opus-4-7"];
|
|
305
|
+
if (/^claude-fable-/iu.test(id)) return CLAUDE_PROFILES["claude-fable-5"];
|
|
306
|
+
return {
|
|
307
|
+
reasoning: true,
|
|
308
|
+
thinkingLevelMap: CLAUDE_XHIGHER,
|
|
309
|
+
input: ["text", "image"],
|
|
310
|
+
compat: CLAUDE_NO_TEMPERATURE
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/discover.ts
|
|
315
|
+
async function discoverChatModels(options) {
|
|
316
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
317
|
+
let models = parseChatModels(await fetchJson(fetchImpl, `${options.origin}/api/system/public/capability-docs`, options.signal));
|
|
318
|
+
if (options.apiKey) models = await filterByAccountModels(models, options, fetchImpl);
|
|
319
|
+
return models;
|
|
320
|
+
}
|
|
321
|
+
async function filterByAccountModels(models, options, fetchImpl) {
|
|
322
|
+
const apiKey = options.apiKey;
|
|
323
|
+
if (!apiKey) return models;
|
|
324
|
+
try {
|
|
325
|
+
const listed = await fetchJson(fetchImpl, `${options.origin}/v1/models`, options.signal, apiKey);
|
|
326
|
+
const allowed = new Set((listed.data ?? []).map((model) => model.id?.trim()).filter((id) => Boolean(id)));
|
|
327
|
+
if (allowed.size > 0) return models.filter((model) => allowed.has(model.id));
|
|
328
|
+
return models;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (apiKey.startsWith("ddoa_") && error instanceof CatalogRequestError && error.status === 401) return models;
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function parseChatModels(docs) {
|
|
335
|
+
const models = [];
|
|
336
|
+
const seen = /* @__PURE__ */ new Set();
|
|
337
|
+
for (const capability of docs.data ?? []) {
|
|
338
|
+
const code = capability.capability_code?.trim() ?? "";
|
|
339
|
+
if (code !== "openai_chat_completions" && code !== "claude_messages") continue;
|
|
340
|
+
for (const model of capability.models ?? []) {
|
|
341
|
+
const id = model.model_code?.trim();
|
|
342
|
+
if (!id || !model.token_rate || seen.has(id)) continue;
|
|
343
|
+
seen.add(id);
|
|
344
|
+
models.push(toChatModel(id, model, code));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return models;
|
|
348
|
+
}
|
|
349
|
+
function toChatModel(id, model, capability) {
|
|
350
|
+
const rate = model.token_rate;
|
|
351
|
+
const thinking = chatModelThinking(id);
|
|
352
|
+
const api = chatApi(model, capability);
|
|
353
|
+
return {
|
|
354
|
+
id,
|
|
355
|
+
name: model.display_name?.trim() || id,
|
|
356
|
+
api,
|
|
357
|
+
reasoning: thinking.reasoning,
|
|
358
|
+
...thinking.thinkingLevelMap === void 0 ? {} : { thinkingLevelMap: thinking.thinkingLevelMap },
|
|
359
|
+
...thinking.compat === void 0 ? {} : { compat: thinking.compat },
|
|
360
|
+
input: thinking.input,
|
|
361
|
+
cost: chatCost(api, rate),
|
|
362
|
+
contextWindow: Math.max(1, rate?.context_window_tokens ?? 128e3),
|
|
363
|
+
maxTokens: Math.max(1, rate?.prepay_completion_token_limit ?? 16384)
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function chatCost(api, rate) {
|
|
367
|
+
const input = rate?.input_usd_per_1m_tokens ?? 0;
|
|
368
|
+
const output = rate?.output_usd_per_1m_tokens ?? 0;
|
|
369
|
+
if (api === "anthropic-messages") return {
|
|
370
|
+
input,
|
|
371
|
+
output,
|
|
372
|
+
cacheRead: input * 2 / 20,
|
|
373
|
+
cacheWrite: input * 25 / 20
|
|
374
|
+
};
|
|
375
|
+
return {
|
|
376
|
+
input,
|
|
377
|
+
output,
|
|
378
|
+
cacheRead: rate?.billing_rules?.cache_read_usd_per_1m_tokens ?? input,
|
|
379
|
+
cacheWrite: rate?.billing_rules?.cache_write_usd_per_1m_tokens ?? input
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
var CatalogRequestError = class extends Error {
|
|
383
|
+
status;
|
|
384
|
+
constructor(status) {
|
|
385
|
+
super(status === 401 ? "D.AI 登录已失效。重新执行 omp login dai" : `D.AI 模型目录请求失败(${status})`);
|
|
386
|
+
this.name = "CatalogRequestError";
|
|
387
|
+
this.status = status;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
function chatApi(model, capability) {
|
|
391
|
+
const path = model.integration_docs?.path ?? "";
|
|
392
|
+
if (capability === "claude_messages" || path.includes("/v1/messages")) return "anthropic-messages";
|
|
393
|
+
if (path.includes("/responses")) return "openai-responses";
|
|
394
|
+
return "openai-completions";
|
|
395
|
+
}
|
|
396
|
+
async function fetchJson(fetchImpl, url, signal, apiKey) {
|
|
397
|
+
const headers = new Headers({ accept: "application/json" });
|
|
398
|
+
if (apiKey) headers.set("authorization", `Bearer ${apiKey}`);
|
|
399
|
+
const response = await fetchImpl(url, {
|
|
400
|
+
headers,
|
|
401
|
+
...signal ? { signal } : {}
|
|
402
|
+
});
|
|
403
|
+
if (!response.ok) throw new CatalogRequestError(response.status);
|
|
404
|
+
return await response.json();
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region ../typescript/dist/index.js
|
|
408
|
+
var SdkError = class extends Error {
|
|
409
|
+
name = "SdkError";
|
|
410
|
+
};
|
|
411
|
+
var RequestAbortedError = class extends SdkError {
|
|
412
|
+
name = "RequestAbortedError";
|
|
413
|
+
constructor() {
|
|
414
|
+
super("D.AI media request was aborted");
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
var RequestTimeoutError = class extends SdkError {
|
|
418
|
+
timeoutMs;
|
|
419
|
+
name = "RequestTimeoutError";
|
|
420
|
+
constructor(timeoutMs) {
|
|
421
|
+
super(`D.AI media request timed out after ${timeoutMs}ms`);
|
|
422
|
+
this.timeoutMs = timeoutMs;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
var ApiError = class extends SdkError {
|
|
426
|
+
status;
|
|
427
|
+
errorType;
|
|
428
|
+
code;
|
|
429
|
+
requestId;
|
|
430
|
+
bodyPreview;
|
|
431
|
+
raw;
|
|
432
|
+
name = "ApiError";
|
|
433
|
+
constructor(status, message, errorType, code, requestId, bodyPreview = "", raw) {
|
|
434
|
+
super(`${message} (HTTP ${status})`);
|
|
435
|
+
this.status = status;
|
|
436
|
+
this.errorType = errorType;
|
|
437
|
+
this.code = code;
|
|
438
|
+
this.requestId = requestId;
|
|
439
|
+
this.bodyPreview = bodyPreview;
|
|
440
|
+
this.raw = raw;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
/** What the consent screen requests by default. */
|
|
444
|
+
const DEFAULT_OAUTH_SCOPES = [
|
|
445
|
+
"openid",
|
|
446
|
+
"profile",
|
|
447
|
+
"email",
|
|
448
|
+
"offline_access"
|
|
449
|
+
];
|
|
450
|
+
/**
|
|
451
|
+
* OAuth 2.1 + OIDC client for D.AI as the identity provider.
|
|
452
|
+
*
|
|
453
|
+
* Covers the protocol half of the authorization code flow: discovery, PKCE
|
|
454
|
+
* authorization URLs, code exchange, refresh, revocation and userinfo. The
|
|
455
|
+
* browser redirect, the loopback callback server and token storage belong to
|
|
456
|
+
* the host application; `examples/oauth-demo` in the D.AI repository is a
|
|
457
|
+
* complete Bun reference implementation.
|
|
458
|
+
*
|
|
459
|
+
* Public clients (the default) are PKCE-only. Create the client with a
|
|
460
|
+
* `clientSecret` for confidential clients; token and revocation requests then
|
|
461
|
+
* use HTTP Basic.
|
|
462
|
+
*/
|
|
463
|
+
var OAuthClient = class {
|
|
464
|
+
issuer;
|
|
465
|
+
clientId;
|
|
466
|
+
timeoutMs;
|
|
467
|
+
#clientSecret;
|
|
468
|
+
#redirectUri;
|
|
469
|
+
#scopes;
|
|
470
|
+
#headers;
|
|
471
|
+
#fetch;
|
|
472
|
+
#discovery;
|
|
473
|
+
#jwks;
|
|
474
|
+
constructor(configuration) {
|
|
475
|
+
const clientId = configuration.clientId?.trim() ?? "";
|
|
476
|
+
if (!clientId) throw new SdkError("clientId is required");
|
|
477
|
+
this.issuer = normalizeIssuer(configuration.issuer ?? "https://dai.itbug.shop");
|
|
478
|
+
this.clientId = clientId;
|
|
479
|
+
this.#clientSecret = configuration.clientSecret?.trim() ?? "";
|
|
480
|
+
this.#redirectUri = configuration.redirectUri;
|
|
481
|
+
this.#scopes = normalizeScope(configuration.scopes ?? DEFAULT_OAUTH_SCOPES);
|
|
482
|
+
this.timeoutMs = configuration.timeoutMs ?? 6e4;
|
|
483
|
+
if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) throw new SdkError("timeoutMs must be greater than zero");
|
|
484
|
+
this.#headers = new Headers(configuration.headers);
|
|
485
|
+
const fetchImplementation = configuration.fetch ?? globalThis.fetch;
|
|
486
|
+
if (typeof fetchImplementation !== "function") throw new SdkError("A Fetch API implementation is required");
|
|
487
|
+
this.#fetch = fetchImplementation.bind(globalThis);
|
|
488
|
+
}
|
|
489
|
+
/** Discovery document, fetched once and cached for the client's lifetime. */
|
|
490
|
+
async discover(options = {}) {
|
|
491
|
+
if (this.#discovery) return this.#discovery;
|
|
492
|
+
const { body } = await this.#request("GET", `${this.issuer}/.well-known/openid-configuration`, options);
|
|
493
|
+
this.#discovery = discoveryFromBody(body);
|
|
494
|
+
return this.#discovery;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Builds the browser authorization URL and the PKCE material for one login
|
|
498
|
+
* attempt. Send the user to `url`, keep `state` and `codeVerifier` in the
|
|
499
|
+
* host application, then pass them to {@link exchangeCode}.
|
|
500
|
+
*/
|
|
501
|
+
async authorizationUrl(options = {}) {
|
|
502
|
+
const discovery = await this.discover(options);
|
|
503
|
+
const redirectUri = options.redirectUri ?? this.#redirectUri;
|
|
504
|
+
if (!redirectUri) throw new SdkError("redirectUri is required; pass it here or set it in the configuration");
|
|
505
|
+
const scope = normalizeScope(options.scope ?? this.#scopes);
|
|
506
|
+
if (scope.length === 0) throw new SdkError("At least one scope is required");
|
|
507
|
+
const state = options.state ?? randomToken();
|
|
508
|
+
const nonce = options.nonce ?? randomToken();
|
|
509
|
+
const { codeVerifier, codeChallenge } = await createPkcePair();
|
|
510
|
+
const url = new URL(discovery.authorizationEndpoint);
|
|
511
|
+
url.searchParams.set("client_id", this.clientId);
|
|
512
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
513
|
+
url.searchParams.set("response_type", "code");
|
|
514
|
+
url.searchParams.set("scope", scope.join(" "));
|
|
515
|
+
url.searchParams.set("state", state);
|
|
516
|
+
url.searchParams.set("nonce", nonce);
|
|
517
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
518
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
519
|
+
if (options.prompt !== void 0) url.searchParams.set("prompt", options.prompt);
|
|
520
|
+
const scopeValue = scope.join(" ");
|
|
521
|
+
return {
|
|
522
|
+
url: url.toString(),
|
|
523
|
+
state,
|
|
524
|
+
nonce,
|
|
525
|
+
codeVerifier,
|
|
526
|
+
codeChallenge,
|
|
527
|
+
scope: scopeValue
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
/** Exchanges the callback `code` for tokens. Codes are single-use. */
|
|
531
|
+
async exchangeCode(request, options = {}) {
|
|
532
|
+
const discovery = await this.discover(options);
|
|
533
|
+
const form = new URLSearchParams({
|
|
534
|
+
grant_type: "authorization_code",
|
|
535
|
+
code: request.code,
|
|
536
|
+
client_id: this.clientId,
|
|
537
|
+
code_verifier: request.codeVerifier
|
|
538
|
+
});
|
|
539
|
+
const redirectUri = request.redirectUri ?? this.#redirectUri;
|
|
540
|
+
if (redirectUri) form.set("redirect_uri", redirectUri);
|
|
541
|
+
const { body } = await this.#request("POST", discovery.tokenEndpoint, {
|
|
542
|
+
...options,
|
|
543
|
+
form,
|
|
544
|
+
basic: this.#clientSecret.length > 0
|
|
545
|
+
});
|
|
546
|
+
return tokensFromBody(body);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Rotates the refresh token. D.AI invalidates the old one immediately;
|
|
550
|
+
* replaying a rotated token revokes the whole token family server-side.
|
|
551
|
+
*/
|
|
552
|
+
async refresh(refreshToken, options = {}) {
|
|
553
|
+
const discovery = await this.discover(options);
|
|
554
|
+
const form = new URLSearchParams({
|
|
555
|
+
grant_type: "refresh_token",
|
|
556
|
+
refresh_token: refreshToken,
|
|
557
|
+
client_id: this.clientId
|
|
558
|
+
});
|
|
559
|
+
if (options.scope !== void 0) {
|
|
560
|
+
const scope = normalizeScope(options.scope);
|
|
561
|
+
if (scope.length === 0) throw new SdkError("scope cannot be empty when narrowing a refresh");
|
|
562
|
+
form.set("scope", scope.join(" "));
|
|
563
|
+
}
|
|
564
|
+
const { body } = await this.#request("POST", discovery.tokenEndpoint, {
|
|
565
|
+
...options,
|
|
566
|
+
form,
|
|
567
|
+
basic: this.#clientSecret.length > 0
|
|
568
|
+
});
|
|
569
|
+
return tokensFromBody(body);
|
|
570
|
+
}
|
|
571
|
+
/** RFC 7009 revocation. Unknown or foreign tokens are accepted silently. */
|
|
572
|
+
async revoke(token, options = {}) {
|
|
573
|
+
const discovery = await this.discover(options);
|
|
574
|
+
if (!discovery.revocationEndpoint) throw new SdkError("The issuer does not advertise a revocation endpoint");
|
|
575
|
+
await this.#request("POST", discovery.revocationEndpoint, {
|
|
576
|
+
...options,
|
|
577
|
+
form: new URLSearchParams({
|
|
578
|
+
token,
|
|
579
|
+
client_id: this.clientId
|
|
580
|
+
}),
|
|
581
|
+
basic: this.#clientSecret.length > 0
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/** OIDC userinfo; fields are trimmed by the granted scope. */
|
|
585
|
+
async userinfo(accessToken, options = {}) {
|
|
586
|
+
const discovery = await this.discover(options);
|
|
587
|
+
const { body } = await this.#request("GET", discovery.userinfoEndpoint, {
|
|
588
|
+
...options,
|
|
589
|
+
bearer: accessToken
|
|
590
|
+
});
|
|
591
|
+
return userInfoFromBody(body);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Verifies an id_token against the issuer's JWKS with WebCrypto (RS256).
|
|
595
|
+
* No JWT dependency is needed. `nonce` is checked only when supplied.
|
|
596
|
+
*/
|
|
597
|
+
async verifyIdToken(idToken, options = {}) {
|
|
598
|
+
const [headerPart, payloadPart, signaturePart] = idToken.split(".");
|
|
599
|
+
if (!headerPart || !payloadPart || !signaturePart) throw new SdkError("id_token is not a valid JWT");
|
|
600
|
+
const header = decodeJsonPart(headerPart);
|
|
601
|
+
const claims = decodeJsonPart(payloadPart);
|
|
602
|
+
const algorithm = header.alg === "RS256";
|
|
603
|
+
const jwks = options.jwks ?? await this.#loadJwks(options);
|
|
604
|
+
const key = jwks.keys.find((candidate) => candidate.kid === header.kid) ?? jwks.keys[0];
|
|
605
|
+
if (!key) throw new SdkError("JWKS contains no usable key");
|
|
606
|
+
const subtle = requireWebCrypto().subtle;
|
|
607
|
+
const cryptoKey = await subtle.importKey("jwk", key, {
|
|
608
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
609
|
+
hash: "SHA-256"
|
|
610
|
+
}, false, ["verify"]);
|
|
611
|
+
const signature = await subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, cryptoKey, decodeBase64UrlBytes(signaturePart), new TextEncoder().encode(`${headerPart}.${payloadPart}`));
|
|
612
|
+
const audience = Array.isArray(claims.aud) ? claims.aud : claims.aud ? [claims.aud] : [];
|
|
613
|
+
const expectedIssuer = options.issuer ?? (await this.discover(options)).issuer;
|
|
614
|
+
const checks = {
|
|
615
|
+
algorithm,
|
|
616
|
+
signature,
|
|
617
|
+
issuer: claims.iss === expectedIssuer,
|
|
618
|
+
audience: audience.includes(options.clientId ?? this.clientId),
|
|
619
|
+
expiry: typeof claims.exp === "number" && claims.exp > Date.now() / 1e3,
|
|
620
|
+
nonce: options.nonce === void 0 || claims.nonce === options.nonce
|
|
621
|
+
};
|
|
622
|
+
return {
|
|
623
|
+
valid: Object.values(checks).every(Boolean),
|
|
624
|
+
header,
|
|
625
|
+
claims,
|
|
626
|
+
checks
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
async #loadJwks(options) {
|
|
630
|
+
if (this.#jwks) return this.#jwks;
|
|
631
|
+
const discovery = await this.discover(options);
|
|
632
|
+
const { body } = await this.#request("GET", discovery.jwksUri, options);
|
|
633
|
+
const keys = body.keys;
|
|
634
|
+
if (!Array.isArray(keys)) throw new SdkError("The JWKS document has no keys");
|
|
635
|
+
this.#jwks = { keys };
|
|
636
|
+
return this.#jwks;
|
|
637
|
+
}
|
|
638
|
+
async #request(method, url, options) {
|
|
639
|
+
const controller = new AbortController();
|
|
640
|
+
const forwardAbort = () => controller.abort(options.signal?.reason);
|
|
641
|
+
if (options.signal) {
|
|
642
|
+
if (options.signal.aborted) forwardAbort();
|
|
643
|
+
else options.signal.addEventListener("abort", forwardAbort, { once: true });
|
|
644
|
+
}
|
|
645
|
+
const timeout = setTimeout(() => controller.abort(new RequestTimeoutError(this.timeoutMs)), this.timeoutMs);
|
|
646
|
+
try {
|
|
647
|
+
const headers = new Headers(this.#headers);
|
|
648
|
+
mergeHeaders(headers, options.headers);
|
|
649
|
+
let body;
|
|
650
|
+
if (options.form) {
|
|
651
|
+
body = options.form;
|
|
652
|
+
headers.set("content-type", "application/x-www-form-urlencoded");
|
|
653
|
+
}
|
|
654
|
+
if (options.bearer !== void 0) headers.set("authorization", `Bearer ${options.bearer}`);
|
|
655
|
+
if (options.basic) {
|
|
656
|
+
if (this.#clientSecret.length === 0) throw new SdkError("A client secret is required for confidential client auth");
|
|
657
|
+
headers.set("authorization", `Basic ${base64Encode(new TextEncoder().encode(`${this.clientId}:${this.#clientSecret}`))}`);
|
|
658
|
+
}
|
|
659
|
+
const response = await this.#fetch(url, {
|
|
660
|
+
method,
|
|
661
|
+
headers,
|
|
662
|
+
...body === void 0 ? {} : { body },
|
|
663
|
+
signal: controller.signal,
|
|
664
|
+
redirect: "manual"
|
|
665
|
+
});
|
|
666
|
+
const text = await response.text();
|
|
667
|
+
let body_ = {};
|
|
668
|
+
if (text.length > 0) try {
|
|
669
|
+
const parsed = JSON.parse(text);
|
|
670
|
+
if (isJsonObject(parsed)) body_ = parsed;
|
|
671
|
+
} catch {}
|
|
672
|
+
if (!response.ok) throw apiErrorFrom(response, body_, text);
|
|
673
|
+
return {
|
|
674
|
+
status: response.status,
|
|
675
|
+
body: body_
|
|
676
|
+
};
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if (controller.signal.aborted) {
|
|
679
|
+
const reason = controller.signal.reason;
|
|
680
|
+
if (reason instanceof RequestTimeoutError) throw reason;
|
|
681
|
+
throw new RequestAbortedError();
|
|
682
|
+
}
|
|
683
|
+
throw error;
|
|
684
|
+
} finally {
|
|
685
|
+
clearTimeout(timeout);
|
|
686
|
+
options.signal?.removeEventListener("abort", forwardAbort);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
function normalizeIssuer(value) {
|
|
691
|
+
const issuer = value.trim().replace(/\/+$/, "");
|
|
692
|
+
if (!issuer) throw new SdkError("issuer is required");
|
|
693
|
+
try {
|
|
694
|
+
new URL(issuer);
|
|
695
|
+
} catch {
|
|
696
|
+
throw new SdkError(`issuer is not a valid URL: ${issuer}`);
|
|
697
|
+
}
|
|
698
|
+
return issuer;
|
|
699
|
+
}
|
|
700
|
+
function normalizeScope(value) {
|
|
701
|
+
return (typeof value === "string" ? value.split(/\s+/) : [...value]).map((item) => item.trim()).filter((item) => item.length > 0);
|
|
702
|
+
}
|
|
703
|
+
function isJsonObject(value) {
|
|
704
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
705
|
+
}
|
|
706
|
+
function stringValue(value) {
|
|
707
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
708
|
+
}
|
|
709
|
+
function stringArray(value) {
|
|
710
|
+
if (!Array.isArray(value)) return [];
|
|
711
|
+
return value.filter((item) => typeof item === "string");
|
|
712
|
+
}
|
|
713
|
+
function discoveryFromBody(body) {
|
|
714
|
+
const issuer = stringValue(body.issuer);
|
|
715
|
+
const authorizationEndpoint = stringValue(body.authorization_endpoint);
|
|
716
|
+
const tokenEndpoint = stringValue(body.token_endpoint);
|
|
717
|
+
const userinfoEndpoint = stringValue(body.userinfo_endpoint);
|
|
718
|
+
const jwksUri = stringValue(body.jwks_uri);
|
|
719
|
+
const revocationEndpoint = stringValue(body.revocation_endpoint);
|
|
720
|
+
if (!issuer || !authorizationEndpoint || !tokenEndpoint || !userinfoEndpoint || !jwksUri) throw new SdkError("The discovery document is missing required endpoints");
|
|
721
|
+
return {
|
|
722
|
+
issuer,
|
|
723
|
+
authorizationEndpoint,
|
|
724
|
+
tokenEndpoint,
|
|
725
|
+
userinfoEndpoint,
|
|
726
|
+
jwksUri,
|
|
727
|
+
...revocationEndpoint === void 0 ? {} : { revocationEndpoint },
|
|
728
|
+
scopesSupported: stringArray(body.scopes_supported),
|
|
729
|
+
responseTypesSupported: stringArray(body.response_types_supported),
|
|
730
|
+
grantTypesSupported: stringArray(body.grant_types_supported),
|
|
731
|
+
codeChallengeMethodsSupported: stringArray(body.code_challenge_methods_supported),
|
|
732
|
+
tokenEndpointAuthMethodsSupported: stringArray(body.token_endpoint_auth_methods_supported),
|
|
733
|
+
idTokenSigningAlgValuesSupported: stringArray(body.id_token_signing_alg_values_supported),
|
|
734
|
+
claimsSupported: stringArray(body.claims_supported)
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function tokensFromBody(body) {
|
|
738
|
+
const accessToken = stringValue(body.access_token);
|
|
739
|
+
if (!accessToken) throw new SdkError("The token response is missing access_token");
|
|
740
|
+
const expiresIn = typeof body.expires_in === "number" ? body.expires_in : NaN;
|
|
741
|
+
if (!Number.isFinite(expiresIn)) throw new SdkError("The token response is missing expires_in");
|
|
742
|
+
const refreshToken = stringValue(body.refresh_token);
|
|
743
|
+
const idToken = stringValue(body.id_token);
|
|
744
|
+
return {
|
|
745
|
+
accessToken,
|
|
746
|
+
tokenType: stringValue(body.token_type) ?? "Bearer",
|
|
747
|
+
expiresIn,
|
|
748
|
+
scope: stringValue(body.scope) ?? "",
|
|
749
|
+
...refreshToken === void 0 ? {} : { refreshToken },
|
|
750
|
+
...idToken === void 0 ? {} : { idToken }
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function userInfoFromBody(body) {
|
|
754
|
+
const sub = stringValue(body.sub);
|
|
755
|
+
if (!sub) throw new SdkError("The userinfo response is missing sub");
|
|
756
|
+
const optional = {};
|
|
757
|
+
for (const [field, key] of [
|
|
758
|
+
["preferred_username", "preferredUsername"],
|
|
759
|
+
["name", "name"],
|
|
760
|
+
["picture", "picture"],
|
|
761
|
+
["email", "email"],
|
|
762
|
+
["role", "role"],
|
|
763
|
+
["user_tier", "userTier"]
|
|
764
|
+
]) {
|
|
765
|
+
const value = stringValue(body[field]);
|
|
766
|
+
if (value !== void 0) optional[key] = value;
|
|
767
|
+
}
|
|
768
|
+
if (typeof body.email_verified === "boolean") optional.emailVerified = body.email_verified;
|
|
769
|
+
return {
|
|
770
|
+
sub,
|
|
771
|
+
...optional
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function apiErrorFrom(response, body, text) {
|
|
775
|
+
const errorType = stringValue(body.error);
|
|
776
|
+
const message = stringValue(body.error_description) ?? errorType ?? response.statusText ?? `HTTP ${response.status}`;
|
|
777
|
+
return new ApiError(response.status, message, errorType, void 0, response.headers.get("x-request-id") ?? void 0, text.slice(0, 2e3), body);
|
|
778
|
+
}
|
|
779
|
+
function mergeHeaders(target, source) {
|
|
780
|
+
if (source === void 0) return;
|
|
781
|
+
for (const [name, value] of new Headers(source)) target.set(name, value);
|
|
782
|
+
}
|
|
783
|
+
function requireWebCrypto() {
|
|
784
|
+
const crypto = globalThis.crypto;
|
|
785
|
+
if (!crypto?.subtle) throw new SdkError("WebCrypto is required for PKCE and id_token verification");
|
|
786
|
+
return crypto;
|
|
787
|
+
}
|
|
788
|
+
function randomToken(bytes = 32) {
|
|
789
|
+
const buffer = new Uint8Array(bytes);
|
|
790
|
+
requireWebCrypto().getRandomValues(buffer);
|
|
791
|
+
return base64UrlEncode(buffer);
|
|
792
|
+
}
|
|
793
|
+
async function createPkcePair() {
|
|
794
|
+
const codeVerifier = randomToken(32);
|
|
795
|
+
const digest = await requireWebCrypto().subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
|
|
796
|
+
return {
|
|
797
|
+
codeVerifier,
|
|
798
|
+
codeChallenge: base64UrlEncode(new Uint8Array(digest))
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function base64Encode(bytes) {
|
|
802
|
+
let binary = "";
|
|
803
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
804
|
+
return btoa(binary);
|
|
805
|
+
}
|
|
806
|
+
function base64UrlEncode(bytes) {
|
|
807
|
+
return base64Encode(bytes).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
808
|
+
}
|
|
809
|
+
function decodeBase64UrlBytes(value) {
|
|
810
|
+
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
811
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
812
|
+
const binary = atob(padded);
|
|
813
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
814
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
815
|
+
return bytes;
|
|
816
|
+
}
|
|
817
|
+
function decodeJsonPart(part) {
|
|
818
|
+
try {
|
|
819
|
+
const parsed = JSON.parse(new TextDecoder().decode(decodeBase64UrlBytes(part)));
|
|
820
|
+
if (!isJsonObject(parsed)) throw new Error("not an object");
|
|
821
|
+
return parsed;
|
|
822
|
+
} catch {
|
|
823
|
+
throw new SdkError("id_token contains an invalid JWT segment");
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region src/target.ts
|
|
828
|
+
const PROD_ORIGIN = "https://dai.itbug.shop";
|
|
829
|
+
const PROD_CLIENT_ID = "ddc_Go98eEOhzDjmvw5k09CM";
|
|
830
|
+
const DEV_CLIENT_ID = "ddc_SgjSciyD25ZhE6zJ7ZVU";
|
|
831
|
+
const CALLBACK_HOST = "127.0.0.1";
|
|
832
|
+
const CALLBACK_PORT = 53682;
|
|
833
|
+
const CALLBACK_URL = `http://${CALLBACK_HOST}:${CALLBACK_PORT}/callback`;
|
|
834
|
+
const OAUTH_SCOPES = [
|
|
835
|
+
"openid",
|
|
836
|
+
"profile",
|
|
837
|
+
"email",
|
|
838
|
+
"offline_access",
|
|
839
|
+
"media"
|
|
840
|
+
];
|
|
841
|
+
function explicitDaiOrigin(env = process.env) {
|
|
842
|
+
const value = firstNonEmpty(env.DAI_ISSUER, env.DAI_BASE_URL);
|
|
843
|
+
return value === void 0 ? void 0 : normalizeOrigin(value);
|
|
844
|
+
}
|
|
845
|
+
function resolveDaiTarget(env = process.env) {
|
|
846
|
+
const origin = explicitDaiOrigin(env) ?? "https://dai.itbug.shop";
|
|
847
|
+
const override = env.DAI_OMP_CLIENT_ID?.trim();
|
|
848
|
+
if (override) return {
|
|
849
|
+
origin,
|
|
850
|
+
clientId: override
|
|
851
|
+
};
|
|
852
|
+
if (isLoopbackOrigin(origin)) return {
|
|
853
|
+
origin,
|
|
854
|
+
clientId: DEV_CLIENT_ID
|
|
855
|
+
};
|
|
856
|
+
if (origin === "https://dai.itbug.shop") return {
|
|
857
|
+
origin,
|
|
858
|
+
clientId: PROD_CLIENT_ID
|
|
859
|
+
};
|
|
860
|
+
throw new Error(`没有 ${origin} 对应的 OAuth client。设置 DAI_OMP_CLIENT_ID,或使用 ${PROD_ORIGIN}`);
|
|
861
|
+
}
|
|
862
|
+
function normalizeOrigin(value) {
|
|
863
|
+
const trimmed = value.trim();
|
|
864
|
+
if (!trimmed) throw new Error("DAI_BASE_URL 不能为空");
|
|
865
|
+
let url;
|
|
866
|
+
try {
|
|
867
|
+
url = new URL(trimmed);
|
|
868
|
+
} catch {
|
|
869
|
+
throw new Error(`DAI_BASE_URL 不是合法地址:${trimmed}`);
|
|
870
|
+
}
|
|
871
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("DAI_BASE_URL 必须是 http 或 https");
|
|
872
|
+
const path = url.pathname.replace(/\/+$/u, "").replace(/\/v1$/u, "");
|
|
873
|
+
if (path && path !== "/") throw new Error("DAI_BASE_URL 只能是站点根地址,不要带路径");
|
|
874
|
+
return url.origin;
|
|
875
|
+
}
|
|
876
|
+
function isLoopbackOrigin(origin) {
|
|
877
|
+
const hostname = new URL(origin).hostname;
|
|
878
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]" || hostname === "::1";
|
|
879
|
+
}
|
|
880
|
+
function firstNonEmpty(...values) {
|
|
881
|
+
for (const value of values) {
|
|
882
|
+
const trimmed = value?.trim();
|
|
883
|
+
if (trimmed) return trimmed;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/callback.ts
|
|
888
|
+
const CALLBACK_TIMEOUT_MS = 18e4;
|
|
889
|
+
function waitForOAuthCallback(options) {
|
|
890
|
+
return new Promise((resolve, reject) => {
|
|
891
|
+
let settled = false;
|
|
892
|
+
const server = createServer((request, response) => {
|
|
893
|
+
handleCallback(request, response, options.expectedState, finish);
|
|
894
|
+
});
|
|
895
|
+
const timer = setTimeout(() => {
|
|
896
|
+
finish(/* @__PURE__ */ new Error("浏览器授权超时(3 分钟)。重新执行 omp login dai"));
|
|
897
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
898
|
+
const onAbort = () => {
|
|
899
|
+
finish(/* @__PURE__ */ new Error("登录已取消"));
|
|
900
|
+
};
|
|
901
|
+
function finish(error, code) {
|
|
902
|
+
if (settled) return;
|
|
903
|
+
settled = true;
|
|
904
|
+
clearTimeout(timer);
|
|
905
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
906
|
+
server.close();
|
|
907
|
+
if (error) {
|
|
908
|
+
reject(error);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
resolve(code ?? "");
|
|
912
|
+
}
|
|
913
|
+
if (options.signal?.aborted) {
|
|
914
|
+
finish(/* @__PURE__ */ new Error("登录已取消"));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
918
|
+
server.on("error", (error) => {
|
|
919
|
+
if (error.code === "EADDRINUSE") {
|
|
920
|
+
finish(/* @__PURE__ */ new Error("回调端口 53682 已被占用。关掉占用它的程序后重试"));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
finish(error);
|
|
924
|
+
});
|
|
925
|
+
server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
|
|
926
|
+
try {
|
|
927
|
+
options.onListening?.();
|
|
928
|
+
} catch (error) {
|
|
929
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
function handleCallback(request, response, expectedState, finish) {
|
|
935
|
+
const url = new URL(request.url ?? "/", CALLBACK_URL);
|
|
936
|
+
if (request.method !== "GET" || url.pathname !== "/callback") {
|
|
937
|
+
sendHtml(response, 404, "找不到页面");
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const oauthError = url.searchParams.get("error");
|
|
941
|
+
if (oauthError) {
|
|
942
|
+
const description = url.searchParams.get("error_description") ?? oauthError;
|
|
943
|
+
sendHtml(response, 400, "授权没有完成,回到终端查看原因。", () => {
|
|
944
|
+
finish(/* @__PURE__ */ new Error(`D.AI 授权失败:${description}`));
|
|
945
|
+
});
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const state = url.searchParams.get("state");
|
|
949
|
+
const code = url.searchParams.get("code")?.trim();
|
|
950
|
+
if (state !== expectedState || !code) {
|
|
951
|
+
sendHtml(response, 400, "授权回调无效。回到终端重新登录。", () => {
|
|
952
|
+
finish(/* @__PURE__ */ new Error("授权回调的 state 或 code 无效"));
|
|
953
|
+
});
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
sendHtml(response, 200, "D.AI 授权完成。可以关闭这个页面,回到 omp。", () => {
|
|
957
|
+
finish(void 0, code);
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
function sendHtml(response, status, message, onDone) {
|
|
961
|
+
const body = `<!doctype html><meta charset="utf-8"><title>D.AI</title><p>${escapeHtml(message)}</p>`;
|
|
962
|
+
if (onDone) response.on("finish", onDone);
|
|
963
|
+
response.writeHead(status, {
|
|
964
|
+
"content-type": "text/html; charset=utf-8",
|
|
965
|
+
"cache-control": "no-store"
|
|
966
|
+
});
|
|
967
|
+
response.end(body);
|
|
968
|
+
}
|
|
969
|
+
function escapeHtml(value) {
|
|
970
|
+
return value.replace(/[&<>"]/gu, (char) => {
|
|
971
|
+
switch (char) {
|
|
972
|
+
case "&": return "&";
|
|
973
|
+
case "<": return "<";
|
|
974
|
+
case ">": return ">";
|
|
975
|
+
default: return """;
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src/login.ts
|
|
981
|
+
async function loginDai(callbacks, env = process.env) {
|
|
982
|
+
const choice = (await callbacks.onPrompt({
|
|
983
|
+
message: "D.AI 登录方式:1 浏览器授权,2 粘贴 API key",
|
|
984
|
+
placeholder: "1"
|
|
985
|
+
})).trim().toLowerCase();
|
|
986
|
+
if (choice === "2" || choice === "key" || choice === "apikey") return promptApiKey(callbacks);
|
|
987
|
+
if (choice === "1" || choice === "oauth") return browserLogin(callbacks, resolveDaiTarget(env));
|
|
988
|
+
throw new Error("请输入 1(浏览器授权)或 2(粘贴 API key)");
|
|
989
|
+
}
|
|
990
|
+
async function refreshDaiToken(credentials, env = process.env, signal) {
|
|
991
|
+
if (credentials.access.startsWith("ddsk_")) return credentials;
|
|
992
|
+
const target = targetFromCredentials(credentials, env);
|
|
993
|
+
const client = oauthClient(target);
|
|
994
|
+
const request = signal ? { signal } : {};
|
|
995
|
+
const tokens = await client.refresh(credentials.refresh, request);
|
|
996
|
+
if (!tokens.refreshToken) throw new Error("刷新结果没有 refresh token。重新执行 omp login dai");
|
|
997
|
+
return {
|
|
998
|
+
access: tokens.accessToken,
|
|
999
|
+
refresh: tokens.refreshToken,
|
|
1000
|
+
expires: Date.now() + tokens.expiresIn * 1e3,
|
|
1001
|
+
apiEndpoint: target.origin,
|
|
1002
|
+
projectId: target.clientId,
|
|
1003
|
+
...credentials.accountId === void 0 ? {} : { accountId: credentials.accountId },
|
|
1004
|
+
...credentials.email === void 0 ? {} : { email: credentials.email }
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function daiApiKey(credentials) {
|
|
1008
|
+
return credentials.access;
|
|
1009
|
+
}
|
|
1010
|
+
async function promptApiKey(callbacks) {
|
|
1011
|
+
const key = (await callbacks.onPrompt({
|
|
1012
|
+
message: "D.AI API key(ddsk_...)",
|
|
1013
|
+
secret: true
|
|
1014
|
+
})).trim();
|
|
1015
|
+
if (!key.startsWith("ddsk_")) throw new Error("API key 必须以 ddsk_ 开头");
|
|
1016
|
+
return key;
|
|
1017
|
+
}
|
|
1018
|
+
async function browserLogin(callbacks, target) {
|
|
1019
|
+
const client = oauthClient(target);
|
|
1020
|
+
const request = callbacks.signal ? { signal: callbacks.signal } : {};
|
|
1021
|
+
let pending;
|
|
1022
|
+
try {
|
|
1023
|
+
pending = await client.authorizationUrl(request);
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
throw new Error(loginError(error));
|
|
1026
|
+
}
|
|
1027
|
+
callbacks.onProgress?.(`打开浏览器登录 ${target.origin}`);
|
|
1028
|
+
const code = await waitForOAuthCallback({
|
|
1029
|
+
expectedState: pending.state,
|
|
1030
|
+
...callbacks.signal ? { signal: callbacks.signal } : {},
|
|
1031
|
+
onListening() {
|
|
1032
|
+
callbacks.onAuth({
|
|
1033
|
+
url: pending.url,
|
|
1034
|
+
instructions: "在浏览器登录 D.AI 并同意授权。完成后回到终端。"
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
let tokens;
|
|
1039
|
+
try {
|
|
1040
|
+
tokens = await client.exchangeCode({
|
|
1041
|
+
code,
|
|
1042
|
+
codeVerifier: pending.codeVerifier,
|
|
1043
|
+
redirectUri: CALLBACK_URL
|
|
1044
|
+
}, request);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
throw new Error(loginError(error));
|
|
1047
|
+
}
|
|
1048
|
+
if (!tokens.refreshToken) throw new Error("授权结果没有 refresh token。确认客户端允许 offline_access");
|
|
1049
|
+
const identity = await readIdentity(client, tokens.accessToken, callbacks.signal);
|
|
1050
|
+
return {
|
|
1051
|
+
access: tokens.accessToken,
|
|
1052
|
+
refresh: tokens.refreshToken,
|
|
1053
|
+
expires: Date.now() + tokens.expiresIn * 1e3,
|
|
1054
|
+
apiEndpoint: target.origin,
|
|
1055
|
+
projectId: target.clientId,
|
|
1056
|
+
...identity.accountId === void 0 ? {} : { accountId: identity.accountId },
|
|
1057
|
+
...identity.email === void 0 ? {} : { email: identity.email }
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function oauthClient(target) {
|
|
1061
|
+
return new OAuthClient({
|
|
1062
|
+
issuer: target.origin,
|
|
1063
|
+
clientId: target.clientId,
|
|
1064
|
+
redirectUri: CALLBACK_URL,
|
|
1065
|
+
scopes: OAUTH_SCOPES
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
function targetFromCredentials(credentials, env) {
|
|
1069
|
+
if (credentials.apiEndpoint && credentials.projectId) return {
|
|
1070
|
+
origin: credentials.apiEndpoint,
|
|
1071
|
+
clientId: credentials.projectId
|
|
1072
|
+
};
|
|
1073
|
+
return resolveDaiTarget({
|
|
1074
|
+
...env,
|
|
1075
|
+
...credentials.apiEndpoint === void 0 ? {} : { DAI_ISSUER: credentials.apiEndpoint }
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
async function readIdentity(client, accessToken, signal) {
|
|
1079
|
+
try {
|
|
1080
|
+
const info = await client.userinfo(accessToken, signal ? { signal } : {});
|
|
1081
|
+
return {
|
|
1082
|
+
accountId: info.sub,
|
|
1083
|
+
...info.email ? { email: info.email } : {}
|
|
1084
|
+
};
|
|
1085
|
+
} catch {
|
|
1086
|
+
return {};
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function loginError(error) {
|
|
1090
|
+
if (error instanceof SdkError) return `D.AI 登录失败:${error.message}`;
|
|
1091
|
+
if (error instanceof Error) return error.message;
|
|
1092
|
+
return "D.AI 登录失败";
|
|
1093
|
+
}
|
|
1094
|
+
//#endregion
|
|
1095
|
+
//#region src/models.ts
|
|
1096
|
+
const EFFORTS = [
|
|
1097
|
+
"minimal",
|
|
1098
|
+
"low",
|
|
1099
|
+
"medium",
|
|
1100
|
+
"high",
|
|
1101
|
+
"xhigh",
|
|
1102
|
+
"max"
|
|
1103
|
+
];
|
|
1104
|
+
function toOmpChatModels(models, origin) {
|
|
1105
|
+
return expandClaudeChatModels(models).map((model) => toOmpChatModel(model, origin));
|
|
1106
|
+
}
|
|
1107
|
+
function chatModelBaseUrl(origin, api) {
|
|
1108
|
+
return api === "anthropic-messages" ? origin : `${origin}/v1`;
|
|
1109
|
+
}
|
|
1110
|
+
function toOmpChatModel(model, origin) {
|
|
1111
|
+
const thinking = ompThinking(model);
|
|
1112
|
+
return {
|
|
1113
|
+
id: model.id,
|
|
1114
|
+
name: model.name,
|
|
1115
|
+
api: model.api,
|
|
1116
|
+
baseUrl: chatModelBaseUrl(origin, model.api),
|
|
1117
|
+
reasoning: model.reasoning,
|
|
1118
|
+
input: model.input,
|
|
1119
|
+
cost: model.cost,
|
|
1120
|
+
contextWindow: model.contextWindow,
|
|
1121
|
+
maxTokens: model.maxTokens,
|
|
1122
|
+
supportsTools: true,
|
|
1123
|
+
...thinking
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function ompThinking(model) {
|
|
1127
|
+
const map = model.thinkingLevelMap;
|
|
1128
|
+
if (!model.reasoning || map === void 0) return {};
|
|
1129
|
+
const efforts = EFFORTS.filter((level) => typeof map[level] === "string");
|
|
1130
|
+
const effortMap = {};
|
|
1131
|
+
for (const level of efforts) {
|
|
1132
|
+
const wire = map[level];
|
|
1133
|
+
if (typeof wire === "string" && wire !== level) effortMap[level] = wire;
|
|
1134
|
+
}
|
|
1135
|
+
const mapped = Object.keys(effortMap).length > 0;
|
|
1136
|
+
return {
|
|
1137
|
+
thinking: {
|
|
1138
|
+
efforts,
|
|
1139
|
+
requiresEffort: map.off === null || map.off === void 0,
|
|
1140
|
+
...mapped ? { effortMap } : {}
|
|
1141
|
+
},
|
|
1142
|
+
...model.api === "anthropic-messages" ? {} : { compat: {
|
|
1143
|
+
supportsReasoningEffort: true,
|
|
1144
|
+
...mapped ? { reasoningEffortMap: effortMap } : {}
|
|
1145
|
+
} }
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region src/stored-origin.ts
|
|
1150
|
+
async function resolveDiscoveryOrigin(apiKey, env = process.env) {
|
|
1151
|
+
const explicit = explicitDaiOrigin(env);
|
|
1152
|
+
if (explicit !== void 0) return explicit;
|
|
1153
|
+
if (apiKey?.startsWith("ddoa_")) {
|
|
1154
|
+
const stored = await readStoredOauthOrigin(env);
|
|
1155
|
+
if (stored !== void 0) return stored;
|
|
1156
|
+
}
|
|
1157
|
+
return PROD_ORIGIN;
|
|
1158
|
+
}
|
|
1159
|
+
async function readStoredOauthOrigin(env) {
|
|
1160
|
+
const dbPath = agentDbPath(env);
|
|
1161
|
+
if (!existsSync(dbPath)) return;
|
|
1162
|
+
let db;
|
|
1163
|
+
try {
|
|
1164
|
+
db = await openReadonly(dbPath);
|
|
1165
|
+
if (db === void 0) return;
|
|
1166
|
+
const row = db.get("SELECT data FROM auth_credentials WHERE provider = ? AND credential_type = ? ORDER BY updated_at DESC LIMIT 1", "dai", "oauth");
|
|
1167
|
+
if (row === void 0) return;
|
|
1168
|
+
const parsed = JSON.parse(row.data);
|
|
1169
|
+
return typeof parsed.apiEndpoint === "string" ? normalizeOrigin(parsed.apiEndpoint) : void 0;
|
|
1170
|
+
} catch {
|
|
1171
|
+
return;
|
|
1172
|
+
} finally {
|
|
1173
|
+
db?.close();
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function agentDbPath(env) {
|
|
1177
|
+
const override = env.PI_CODING_AGENT_DIR?.trim();
|
|
1178
|
+
if (override) return join(override, "agent.db");
|
|
1179
|
+
const configRoot = env.PI_CONFIG_DIR?.trim() || join(homedir(), ".omp");
|
|
1180
|
+
const profile = env.OMP_PROFILE?.trim() || env.PI_PROFILE?.trim();
|
|
1181
|
+
const agentDir = profile ? join(configRoot, "profiles", profile, "agent") : join(configRoot, "agent");
|
|
1182
|
+
return join(agentDir, "agent.db");
|
|
1183
|
+
}
|
|
1184
|
+
async function openReadonly(dbPath) {
|
|
1185
|
+
const bunDb = await openBun(dbPath);
|
|
1186
|
+
if (bunDb !== void 0) return bunDb;
|
|
1187
|
+
return openNode(dbPath);
|
|
1188
|
+
}
|
|
1189
|
+
async function openBun(dbPath) {
|
|
1190
|
+
try {
|
|
1191
|
+
const db = new (await (import("bun:sqlite"))).Database(dbPath, { readonly: true });
|
|
1192
|
+
return wrap(db.query.bind(db), () => db.close());
|
|
1193
|
+
} catch {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
async function openNode(dbPath) {
|
|
1198
|
+
try {
|
|
1199
|
+
const db = new (await (import("node:sqlite"))).DatabaseSync(dbPath, { readOnly: true });
|
|
1200
|
+
return wrap((sql) => db.prepare(sql), () => db.close());
|
|
1201
|
+
} catch {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function wrap(prepare, close) {
|
|
1206
|
+
return {
|
|
1207
|
+
get(sql, ...params) {
|
|
1208
|
+
const row = prepare(sql).get(...params);
|
|
1209
|
+
if (row === null || row === void 0 || typeof row !== "object" || !("data" in row)) return;
|
|
1210
|
+
const data = row.data;
|
|
1211
|
+
return typeof data === "string" ? { data } : void 0;
|
|
1212
|
+
},
|
|
1213
|
+
close
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
//#endregion
|
|
1217
|
+
//#region src/provider.ts
|
|
1218
|
+
function registerDaiProvider(pi) {
|
|
1219
|
+
pi.on("before_provider_request", (event) => rewriteClaudeProviderPayload(event.payload));
|
|
1220
|
+
pi.registerProvider("dai", {
|
|
1221
|
+
name: "D.AI",
|
|
1222
|
+
baseUrl: "https://dai.itbug.shop/v1",
|
|
1223
|
+
api: "openai-completions",
|
|
1224
|
+
apiKey: "DAI_API_KEY",
|
|
1225
|
+
authHeader: true,
|
|
1226
|
+
models: [],
|
|
1227
|
+
async fetchDynamicModels(apiKey) {
|
|
1228
|
+
const key = typeof apiKey === "string" ? apiKey.trim() : "";
|
|
1229
|
+
const origin = await resolveDiscoveryOrigin(key || void 0);
|
|
1230
|
+
return toOmpChatModels(await discoverChatModels({
|
|
1231
|
+
origin,
|
|
1232
|
+
...key ? { apiKey: key } : {}
|
|
1233
|
+
}), origin);
|
|
1234
|
+
},
|
|
1235
|
+
oauth: {
|
|
1236
|
+
name: "D.AI",
|
|
1237
|
+
login(callbacks) {
|
|
1238
|
+
return loginDai(callbacks);
|
|
1239
|
+
},
|
|
1240
|
+
refreshToken(credentials, signal) {
|
|
1241
|
+
return refreshDaiToken(credentials, process.env, signal);
|
|
1242
|
+
},
|
|
1243
|
+
getApiKey: daiApiKey
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
//#endregion
|
|
1248
|
+
//#region src/index.ts
|
|
1249
|
+
function daiOmp(pi) {
|
|
1250
|
+
registerDaiProvider(pi);
|
|
1251
|
+
}
|
|
1252
|
+
//#endregion
|
|
1253
|
+
export { daiOmp as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-ai/omp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "omp plugin: log in to D.AI and use its text models",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": ["omp-plugin", "pi-package"],
|
|
8
|
+
"homepage": "https://dai.itbug.shop",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/mdddj/dd_server_rust_private.git",
|
|
12
|
+
"directory": "sdks/omp"
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist", "README.md"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsdown",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"check": "npm run typecheck && npm run build",
|
|
19
|
+
"prepublishOnly": "npm run check"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@d-ai/media-sdk": "file:../typescript",
|
|
26
|
+
"@types/node": "^26.6.3",
|
|
27
|
+
"tsdown": "^0.23.0",
|
|
28
|
+
"typescript": "^7.0.2"
|
|
29
|
+
},
|
|
30
|
+
"omp": {
|
|
31
|
+
"extensions": ["./dist/index.js"]
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|