omnigateway 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/omni.js +527 -228
- package/gateway.js +763 -401
- package/package.json +1 -1
- package/public/assets/{Chip-BB_5C1Zp.js → Chip-C4X8tf5z.js} +1 -1
- package/public/assets/Confirm-BnmH8Gn0.js +4 -0
- package/public/assets/{CopyValue-CRQDLo7k.js → CopyValue-B-uR4Js-.js} +5 -5
- package/public/assets/{Field-uHZxl4fI.js → Field-Ct4SeDZD.js} +2 -2
- package/public/assets/{Lamp-B-5SjXbG.js → Lamp-CBOwqG5K.js} +7 -7
- package/public/assets/{Meter-DI_BRUKt.js → Meter-COdz0dwY.js} +1 -1
- package/public/assets/{Modal-CI6jk2D4.js → Modal-DMFUHQ-A.js} +8 -8
- package/public/assets/{Rack-D1WJswv3.js → Rack-DAbY9x1I.js} +18 -18
- package/public/assets/{Readout-BocZ2HXP.js → Readout-Bl2kD9Tl.js} +6 -2
- package/public/assets/{States-Bbiu5cHE.js → States-D5Memn8D.js} +4 -4
- package/public/assets/{Table-CdPWxYaz.js → Table-DObRatbW.js} +1 -1
- package/public/assets/{Toggle-CiLC67Dw.js → Toggle-Dkjlvck5.js} +1 -1
- package/public/assets/{TokenBreakdown-B96iPBm9.js → TokenBreakdown-oXP49ZTa.js} +5 -2
- package/public/assets/_app-Brc2wQZm.js +1 -0
- package/public/assets/_app.accounts-I9RXUiyi.js +54 -0
- package/public/assets/{_app.console-Daz4aKhf.js → _app.console-BVVpryIB.js} +10 -10
- package/public/assets/_app.index-83Ig4FUL.js +62 -0
- package/public/assets/_app.keys-KeP6L4g_.js +39 -0
- package/public/assets/_app.logs-B5oh01Ls.js +32 -0
- package/public/assets/_app.models-BuOFMZUN.js +144 -0
- package/public/assets/{_app.settings-a0FKyQAi.js → _app.settings-BN436vI2.js} +4 -4
- package/public/assets/_app.usage-C528VOia.js +166 -0
- package/public/assets/catalog-kJ53n_fc.js +1 -0
- package/public/assets/{dist-C-IbPRiV.js → dist-CnzT-Ut2.js} +1 -1
- package/public/assets/index-CE-KQ-ju.js +172 -0
- package/public/assets/{login-CTvH_KAd.js → login-2LpCZOtB.js} +8 -8
- package/public/assets/{queries-D2o-X8Pj.js → queries-1zRLkX-Q.js} +27 -27
- package/public/assets/{trash-2-BcZb-sCT.js → trash-2-Da6cLgPn.js} +1 -1
- package/public/index.html +2 -2
- package/public/assets/Confirm-B6aAiVbT.js +0 -4
- package/public/assets/_app-BBOF6A0T.js +0 -1
- package/public/assets/_app.accounts-BNkhpvaB.js +0 -54
- package/public/assets/_app.index-LO6d38oe.js +0 -62
- package/public/assets/_app.keys-kBqLqoFf.js +0 -39
- package/public/assets/_app.logs-_wYNS47N.js +0 -32
- package/public/assets/_app.models-Cjng8ohC.js +0 -144
- package/public/assets/_app.usage-D3KtgLrC.js +0 -166
- package/public/assets/catalog-C_OQ0icG.js +0 -1
- package/public/assets/index-PW6EvVh5.js +0 -170
package/bin/omni.js
CHANGED
|
@@ -155,12 +155,14 @@ function createAdminAuth(store, opts) {
|
|
|
155
155
|
var ANTHROPIC_NATIVE_TOOLS = {
|
|
156
156
|
anthropic: true,
|
|
157
157
|
openai: false,
|
|
158
|
-
kimi: false
|
|
158
|
+
kimi: false,
|
|
159
|
+
custom: false
|
|
159
160
|
};
|
|
160
161
|
var PROVIDER_CAPABILITIES = {
|
|
161
162
|
anthropic: { tools: true, images: true, reasoning: true },
|
|
162
163
|
openai: { tools: true, images: true, reasoning: true },
|
|
163
|
-
kimi: { tools: true, images: false, reasoning: false }
|
|
164
|
+
kimi: { tools: true, images: false, reasoning: false },
|
|
165
|
+
custom: { tools: true, images: true, reasoning: true }
|
|
164
166
|
};
|
|
165
167
|
// packages/ir/src/errors.ts
|
|
166
168
|
var RETRYABLE = {
|
|
@@ -174,6 +176,7 @@ var RETRYABLE = {
|
|
|
174
176
|
TIMEOUT: true,
|
|
175
177
|
NETWORK: true,
|
|
176
178
|
BAD_REQUEST: false,
|
|
179
|
+
CONFLICT: false,
|
|
177
180
|
CONTENT_FILTER: false,
|
|
178
181
|
NO_CANDIDATES: false,
|
|
179
182
|
ALL_CANDIDATES_FAILED: false,
|
|
@@ -240,6 +243,82 @@ function usageFromPromptTotal(promptTokens, outputTokens, cacheReadTokens, cache
|
|
|
240
243
|
cacheWriteTokens
|
|
241
244
|
};
|
|
242
245
|
}
|
|
246
|
+
// packages/ir/src/tokens.ts
|
|
247
|
+
var CHARS_PER_TOKEN = 4;
|
|
248
|
+
var IMAGE_TOKENS = 1600;
|
|
249
|
+
var BLOCK_OVERHEAD = 4;
|
|
250
|
+
var MESSAGE_OVERHEAD = 4;
|
|
251
|
+
function fromText(text) {
|
|
252
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
253
|
+
}
|
|
254
|
+
function blockTokens(block) {
|
|
255
|
+
switch (block.type) {
|
|
256
|
+
case "text":
|
|
257
|
+
return BLOCK_OVERHEAD + fromText(block.text);
|
|
258
|
+
case "image":
|
|
259
|
+
return BLOCK_OVERHEAD + IMAGE_TOKENS;
|
|
260
|
+
case "thinking":
|
|
261
|
+
return BLOCK_OVERHEAD + fromText(block.text);
|
|
262
|
+
case "toolUse":
|
|
263
|
+
return BLOCK_OVERHEAD + fromText(block.name) + fromText(safeJson(block.input));
|
|
264
|
+
case "toolResult":
|
|
265
|
+
return BLOCK_OVERHEAD + fromText(block.toolUseId) + fromText(block.content);
|
|
266
|
+
case "anthropicNative":
|
|
267
|
+
return BLOCK_OVERHEAD + fromText(block.blockType) + fromText(safeJson(block.data));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function messageTokens(message) {
|
|
271
|
+
let total = MESSAGE_OVERHEAD;
|
|
272
|
+
for (const block of message.content)
|
|
273
|
+
total += blockTokens(block);
|
|
274
|
+
return total;
|
|
275
|
+
}
|
|
276
|
+
function toolTokens(tool) {
|
|
277
|
+
if (tool.provider === "anthropic") {
|
|
278
|
+
return BLOCK_OVERHEAD + fromText(tool.name) + fromText(tool.type) + fromText(safeJson(tool.wire));
|
|
279
|
+
}
|
|
280
|
+
return BLOCK_OVERHEAD + fromText(tool.name) + fromText(tool.description ?? "") + fromText(safeJson(tool.inputSchema));
|
|
281
|
+
}
|
|
282
|
+
function safeJson(value) {
|
|
283
|
+
try {
|
|
284
|
+
return JSON.stringify(value) ?? "";
|
|
285
|
+
} catch {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function estimateInputTokens(request) {
|
|
290
|
+
let total = 0;
|
|
291
|
+
for (const block of request.system ?? [])
|
|
292
|
+
total += blockTokens(block);
|
|
293
|
+
for (const message of request.messages)
|
|
294
|
+
total += messageTokens(message);
|
|
295
|
+
for (const tool of request.tools ?? [])
|
|
296
|
+
total += toolTokens(tool);
|
|
297
|
+
return total;
|
|
298
|
+
}
|
|
299
|
+
function estimateCachedInputTokens(request) {
|
|
300
|
+
let running = 0;
|
|
301
|
+
let cached = 0;
|
|
302
|
+
for (const tool of request.tools ?? []) {
|
|
303
|
+
running += toolTokens(tool);
|
|
304
|
+
if (tool.cacheControl !== undefined)
|
|
305
|
+
cached = running;
|
|
306
|
+
}
|
|
307
|
+
for (const block of request.system ?? []) {
|
|
308
|
+
running += blockTokens(block);
|
|
309
|
+
if (cacheControlOf(block) !== undefined)
|
|
310
|
+
cached = running;
|
|
311
|
+
}
|
|
312
|
+
for (const message of request.messages) {
|
|
313
|
+
running += MESSAGE_OVERHEAD;
|
|
314
|
+
for (const block of message.content) {
|
|
315
|
+
running += blockTokens(block);
|
|
316
|
+
if (cacheControlOf(block) !== undefined)
|
|
317
|
+
cached = running;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return cached;
|
|
321
|
+
}
|
|
243
322
|
// packages/control/src/config.ts
|
|
244
323
|
var MIN_KEY_LENGTH = 16;
|
|
245
324
|
var TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
@@ -314,7 +393,8 @@ var BODY_ORDER = {
|
|
|
314
393
|
"parallel_tool_calls",
|
|
315
394
|
"metadata"
|
|
316
395
|
],
|
|
317
|
-
kimi: ["model", "messages", "tools", "tool_choice", "max_tokens", "temperature", "stream"]
|
|
396
|
+
kimi: ["model", "messages", "tools", "tool_choice", "max_tokens", "temperature", "stream"],
|
|
397
|
+
custom: []
|
|
318
398
|
};
|
|
319
399
|
function orderFields(obj, order) {
|
|
320
400
|
const out = {};
|
|
@@ -415,10 +495,20 @@ var ANTHROPIC_MODELS = {
|
|
|
415
495
|
id: "claude-haiku-4-5",
|
|
416
496
|
label: "Claude Haiku 4.5",
|
|
417
497
|
pricing: { input: 1, output: 5, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2 },
|
|
418
|
-
limits: { contextWindow: 200000, maxOutputTokens: 64000 }
|
|
498
|
+
limits: { contextWindow: 200000, maxOutputTokens: 64000 },
|
|
499
|
+
reasoningForm: "budget"
|
|
419
500
|
}
|
|
420
501
|
]
|
|
421
502
|
};
|
|
503
|
+
var ONE_M_SUFFIX = "[1m]";
|
|
504
|
+
var DATED_SUFFIX = /-\d{8}$/;
|
|
505
|
+
function anthropicReasoningForm(model) {
|
|
506
|
+
let id = model.trim();
|
|
507
|
+
if (id.toLowerCase().endsWith(ONE_M_SUFFIX))
|
|
508
|
+
id = id.slice(0, -ONE_M_SUFFIX.length).trim();
|
|
509
|
+
id = id.replace(DATED_SUFFIX, "");
|
|
510
|
+
return ANTHROPIC_MODELS.models.find((m) => m.id === id)?.reasoningForm ?? "adaptive";
|
|
511
|
+
}
|
|
422
512
|
|
|
423
513
|
// packages/providers/src/kimi/models.ts
|
|
424
514
|
var KIMI_MODELS = {
|
|
@@ -490,7 +580,8 @@ var OPENAI_MODELS = {
|
|
|
490
580
|
var PROVIDER_MODEL_CATALOG = {
|
|
491
581
|
anthropic: ANTHROPIC_MODELS,
|
|
492
582
|
openai: OPENAI_MODELS,
|
|
493
|
-
kimi: KIMI_MODELS
|
|
583
|
+
kimi: KIMI_MODELS,
|
|
584
|
+
custom: { defaultModel: "", models: [] }
|
|
494
585
|
};
|
|
495
586
|
function catalogPricing(provider, model) {
|
|
496
587
|
return PROVIDER_MODEL_CATALOG[provider]?.models.find((entry) => entry.id === model)?.pricing ?? null;
|
|
@@ -703,7 +794,8 @@ var kimi = {
|
|
|
703
794
|
var PROFILES = {
|
|
704
795
|
anthropic: { ...anthropic, order: envOrder("OMNI_ORDER_ANTHROPIC", anthropic.order) },
|
|
705
796
|
openai: { ...openai, order: envOrder("OMNI_ORDER_OPENAI", openai.order) },
|
|
706
|
-
kimi: { ...kimi, order: envOrder("OMNI_ORDER_KIMI", kimi.order) }
|
|
797
|
+
kimi: { ...kimi, order: envOrder("OMNI_ORDER_KIMI", kimi.order) },
|
|
798
|
+
custom: { headers: [], order: [] }
|
|
707
799
|
};
|
|
708
800
|
|
|
709
801
|
// packages/providers/src/sse.ts
|
|
@@ -1265,6 +1357,45 @@ function encodeToolChoice(c) {
|
|
|
1265
1357
|
return { type: "tool", name: c.name };
|
|
1266
1358
|
}
|
|
1267
1359
|
}
|
|
1360
|
+
function isRecord(value) {
|
|
1361
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1362
|
+
}
|
|
1363
|
+
function withoutEffort(vendor, note2) {
|
|
1364
|
+
const config = vendor.output_config;
|
|
1365
|
+
if (!isRecord(config))
|
|
1366
|
+
return vendor;
|
|
1367
|
+
const entries = Object.entries(config);
|
|
1368
|
+
const kept = entries.filter(([key]) => key !== "effort");
|
|
1369
|
+
if (kept.length === entries.length)
|
|
1370
|
+
return vendor;
|
|
1371
|
+
note2("anthropic:effort-unsupported");
|
|
1372
|
+
if (kept.length === 0) {
|
|
1373
|
+
return Object.fromEntries(Object.entries(vendor).filter(([key]) => key !== "output_config"));
|
|
1374
|
+
}
|
|
1375
|
+
return { ...vendor, output_config: Object.fromEntries(kept) };
|
|
1376
|
+
}
|
|
1377
|
+
var THINKING_ONLY_EDITS = new Set(["clear_thinking_20251015"]);
|
|
1378
|
+
function thinkingIsOff(thinking) {
|
|
1379
|
+
if (!isRecord(thinking))
|
|
1380
|
+
return thinking !== undefined;
|
|
1381
|
+
return thinking.type !== "adaptive" && thinking.type !== "enabled";
|
|
1382
|
+
}
|
|
1383
|
+
function stripUnsupportedEdits(body, note2) {
|
|
1384
|
+
if (!thinkingIsOff(body.thinking))
|
|
1385
|
+
return;
|
|
1386
|
+
const config = body.context_management;
|
|
1387
|
+
if (!isRecord(config) || !Array.isArray(config.edits))
|
|
1388
|
+
return;
|
|
1389
|
+
const kept = config.edits.filter((e) => !(isRecord(e) && THINKING_ONLY_EDITS.has(String(e.type))));
|
|
1390
|
+
if (kept.length === config.edits.length)
|
|
1391
|
+
return;
|
|
1392
|
+
note2("anthropic:clear-thinking-unsupported");
|
|
1393
|
+
if (kept.length === 0 && Object.keys(config).length === 1) {
|
|
1394
|
+
delete body.context_management;
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
body.context_management = { ...config, edits: kept };
|
|
1398
|
+
}
|
|
1268
1399
|
function toWire(req, model, opts) {
|
|
1269
1400
|
const degradations = [];
|
|
1270
1401
|
const note2 = (d) => {
|
|
@@ -1314,12 +1445,20 @@ function toWire(req, model, opts) {
|
|
|
1314
1445
|
if (req.reasoning !== undefined) {
|
|
1315
1446
|
switch (req.reasoning.mode) {
|
|
1316
1447
|
case "adaptive":
|
|
1448
|
+
if (anthropicReasoningForm(model) === "budget") {
|
|
1449
|
+
body.thinking = { type: "disabled" };
|
|
1450
|
+
note2("anthropic:adaptive-thinking-unsupported");
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1317
1453
|
body.thinking = {
|
|
1318
1454
|
type: "adaptive",
|
|
1319
1455
|
...req.reasoning.display === undefined ? {} : { display: req.reasoning.display }
|
|
1320
1456
|
};
|
|
1321
1457
|
if (req.reasoning.effort !== undefined) {
|
|
1322
|
-
body.output_config = {
|
|
1458
|
+
body.output_config = {
|
|
1459
|
+
...isRecord(body.output_config) ? body.output_config : {},
|
|
1460
|
+
effort: req.reasoning.effort
|
|
1461
|
+
};
|
|
1323
1462
|
}
|
|
1324
1463
|
break;
|
|
1325
1464
|
case "budget":
|
|
@@ -1330,7 +1469,9 @@ function toWire(req, model, opts) {
|
|
|
1330
1469
|
break;
|
|
1331
1470
|
}
|
|
1332
1471
|
}
|
|
1333
|
-
|
|
1472
|
+
const vendor = req.vendor?.anthropic ?? {};
|
|
1473
|
+
Object.assign(body, anthropicReasoningForm(model) === "budget" ? withoutEffort(vendor, note2) : vendor);
|
|
1474
|
+
stripUnsupportedEdits(body, note2);
|
|
1334
1475
|
return { body, degradations };
|
|
1335
1476
|
}
|
|
1336
1477
|
|
|
@@ -1394,115 +1535,6 @@ var anthropicAdapter = {
|
|
|
1394
1535
|
return { events: decodeAnthropic(parseSse(res.body)), degradations: notes };
|
|
1395
1536
|
}
|
|
1396
1537
|
};
|
|
1397
|
-
// packages/providers/src/http-client.ts
|
|
1398
|
-
import { request as httpRequest } from "http";
|
|
1399
|
-
import { request as httpsRequest } from "https";
|
|
1400
|
-
import { Readable } from "stream";
|
|
1401
|
-
function nodeHttpClient(options = {}) {
|
|
1402
|
-
const logger2 = options.logger ?? noopLogger;
|
|
1403
|
-
const now = options.now ?? (() => Date.now());
|
|
1404
|
-
return (req) => new Promise((resolve, reject) => {
|
|
1405
|
-
const url = new URL(req.url);
|
|
1406
|
-
const startedAt = now();
|
|
1407
|
-
let traced = false;
|
|
1408
|
-
const trace = (status, failed = false) => {
|
|
1409
|
-
if (traced || !logger2.enabled("debug"))
|
|
1410
|
-
return;
|
|
1411
|
-
traced = true;
|
|
1412
|
-
logger2.debug("upstream http", {
|
|
1413
|
-
provider: req.provider,
|
|
1414
|
-
status,
|
|
1415
|
-
host: url.host,
|
|
1416
|
-
path: url.pathname,
|
|
1417
|
-
durationMs: now() - startedAt,
|
|
1418
|
-
reason: failed ? "transport error" : undefined
|
|
1419
|
-
});
|
|
1420
|
-
};
|
|
1421
|
-
const send = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
1422
|
-
const bodyBytes = Buffer.from(req.body, "utf8");
|
|
1423
|
-
const headers = {};
|
|
1424
|
-
for (const [name, value] of req.headers)
|
|
1425
|
-
headers[name] = value;
|
|
1426
|
-
if (req.body.length > 0 && !hasHeader(req, "content-length")) {
|
|
1427
|
-
headers["Content-Length"] = bodyBytes.byteLength;
|
|
1428
|
-
}
|
|
1429
|
-
const outgoing = send({
|
|
1430
|
-
protocol: url.protocol,
|
|
1431
|
-
hostname: url.hostname,
|
|
1432
|
-
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
1433
|
-
path: `${url.pathname}${url.search}`,
|
|
1434
|
-
method: req.method,
|
|
1435
|
-
headers,
|
|
1436
|
-
setHost: !hasHeader(req, "host")
|
|
1437
|
-
}, (incoming) => {
|
|
1438
|
-
const chunks = [];
|
|
1439
|
-
let buffered = null;
|
|
1440
|
-
const responseHeaders = new Headers;
|
|
1441
|
-
for (const [k, v] of Object.entries(incoming.headers)) {
|
|
1442
|
-
if (Array.isArray(v))
|
|
1443
|
-
for (const one of v)
|
|
1444
|
-
responseHeaders.append(k, one);
|
|
1445
|
-
else if (typeof v === "string")
|
|
1446
|
-
responseHeaders.set(k, v);
|
|
1447
|
-
}
|
|
1448
|
-
trace(incoming.statusCode);
|
|
1449
|
-
resolve({
|
|
1450
|
-
status: incoming.statusCode ?? 0,
|
|
1451
|
-
headers: responseHeaders,
|
|
1452
|
-
body: Readable.toWeb(incoming),
|
|
1453
|
-
text: () => {
|
|
1454
|
-
buffered ??= new Promise((res, rej) => {
|
|
1455
|
-
incoming.on("data", (c) => chunks.push(c));
|
|
1456
|
-
incoming.on("end", () => res(Buffer.concat(chunks).toString("utf8")));
|
|
1457
|
-
incoming.on("error", rej);
|
|
1458
|
-
});
|
|
1459
|
-
return buffered;
|
|
1460
|
-
}
|
|
1461
|
-
});
|
|
1462
|
-
});
|
|
1463
|
-
const onAbort = () => outgoing.destroy(new Error("aborted"));
|
|
1464
|
-
outgoing.on("error", (err) => {
|
|
1465
|
-
req.signal.removeEventListener("abort", onAbort);
|
|
1466
|
-
trace(undefined, true);
|
|
1467
|
-
reject(err);
|
|
1468
|
-
});
|
|
1469
|
-
outgoing.on("close", () => req.signal.removeEventListener("abort", onAbort));
|
|
1470
|
-
if (req.signal.aborted) {
|
|
1471
|
-
outgoing.destroy(new Error("aborted"));
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
req.signal.addEventListener("abort", onAbort, { once: true });
|
|
1475
|
-
if (bodyBytes.byteLength > 0)
|
|
1476
|
-
outgoing.write(bodyBytes);
|
|
1477
|
-
outgoing.end();
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
function hasHeader(req, lowerName) {
|
|
1481
|
-
return req.headers.some(([name]) => name.toLowerCase() === lowerName);
|
|
1482
|
-
}
|
|
1483
|
-
// packages/providers/src/kimi-device.ts
|
|
1484
|
-
import { randomUUID } from "crypto";
|
|
1485
|
-
function mintKimiDevice() {
|
|
1486
|
-
return {
|
|
1487
|
-
deviceId: randomUUID(),
|
|
1488
|
-
deviceName: "MacBook-Pro",
|
|
1489
|
-
deviceModel: "MacBookPro18,3",
|
|
1490
|
-
osVersion: "15.3.1"
|
|
1491
|
-
};
|
|
1492
|
-
}
|
|
1493
|
-
function kimiDeviceHeaders(providerData) {
|
|
1494
|
-
const deviceId = providerData.deviceId;
|
|
1495
|
-
if (typeof deviceId !== "string" || deviceId.length === 0)
|
|
1496
|
-
return [];
|
|
1497
|
-
const str = (v) => typeof v === "string" && v.length > 0 ? v : "unknown";
|
|
1498
|
-
return [
|
|
1499
|
-
["X-Msh-Device-Id", deviceId],
|
|
1500
|
-
["X-Msh-Device-Name", str(providerData.deviceName)],
|
|
1501
|
-
["X-Msh-Device-Model", str(providerData.deviceModel)],
|
|
1502
|
-
["X-Msh-Os-Version", str(providerData.osVersion)]
|
|
1503
|
-
];
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
1538
|
// packages/providers/src/kimi/decode.ts
|
|
1507
1539
|
var FINISH = {
|
|
1508
1540
|
stop: "endTurn",
|
|
@@ -1613,7 +1645,7 @@ function encodeToolChoice2(c) {
|
|
|
1613
1645
|
return { type: "function", function: { name: c.name } };
|
|
1614
1646
|
}
|
|
1615
1647
|
}
|
|
1616
|
-
function toChatWire(req, model) {
|
|
1648
|
+
function toChatWire(req, model, vendor = "kimi") {
|
|
1617
1649
|
const degradations = [];
|
|
1618
1650
|
const note2 = (d) => {
|
|
1619
1651
|
if (!degradations.includes(d))
|
|
@@ -1697,44 +1729,10 @@ function toChatWire(req, model) {
|
|
|
1697
1729
|
body.tool_choice = encodeToolChoice2(req.toolChoice);
|
|
1698
1730
|
if (req.reasoning !== undefined)
|
|
1699
1731
|
note2("kimi:reasoning-dropped");
|
|
1700
|
-
Object.assign(body, req.vendor?.
|
|
1732
|
+
Object.assign(body, req.vendor?.[vendor] ?? {});
|
|
1701
1733
|
return { body, degradations };
|
|
1702
1734
|
}
|
|
1703
1735
|
|
|
1704
|
-
// packages/providers/src/kimi/index.ts
|
|
1705
|
-
var BASE_URL2 = "https://api.kimi.com/coding/v1/chat/completions";
|
|
1706
|
-
var kimiAdapter = {
|
|
1707
|
-
id: "kimi",
|
|
1708
|
-
capabilities: PROVIDER_CAPABILITIES.kimi,
|
|
1709
|
-
async send(req) {
|
|
1710
|
-
const { body, degradations } = toChatWire(req.request, req.model);
|
|
1711
|
-
const token = req.credentials.accessToken ?? req.credentials.apiKey;
|
|
1712
|
-
if (token === null) {
|
|
1713
|
-
throw new GatewayError("AUTH", "kimi credential has no token", { provider: "kimi" });
|
|
1714
|
-
}
|
|
1715
|
-
const protocol = [
|
|
1716
|
-
["Content-Type", "application/json"],
|
|
1717
|
-
["Accept", "text/event-stream"],
|
|
1718
|
-
["Authorization", `Bearer ${token}`],
|
|
1719
|
-
...kimiDeviceHeaders(req.credentials.providerData)
|
|
1720
|
-
];
|
|
1721
|
-
const profile = PROFILES.kimi;
|
|
1722
|
-
const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
|
|
1723
|
-
const res = await req.http({
|
|
1724
|
-
provider: "kimi",
|
|
1725
|
-
url: BASE_URL2,
|
|
1726
|
-
method: "POST",
|
|
1727
|
-
headers,
|
|
1728
|
-
body: JSON.stringify(orderFields({ ...body, stream: true }, BODY_ORDER.kimi)),
|
|
1729
|
-
signal: req.signal
|
|
1730
|
-
});
|
|
1731
|
-
if (res.status < 200 || res.status >= 300)
|
|
1732
|
-
throw await httpError(res, "kimi");
|
|
1733
|
-
if (res.body === null)
|
|
1734
|
-
throw new GatewayError("UPSTREAM", "empty response body", { provider: "kimi" });
|
|
1735
|
-
return { events: decodeChat(parseSse(res.body)), degradations };
|
|
1736
|
-
}
|
|
1737
|
-
};
|
|
1738
1736
|
// packages/providers/src/openai/decode.ts
|
|
1739
1737
|
var ERROR_CODE = {
|
|
1740
1738
|
rate_limit_exceeded: "RATE_LIMIT",
|
|
@@ -2010,6 +2008,190 @@ ${block.text}
|
|
|
2010
2008
|
return { body, degradations };
|
|
2011
2009
|
}
|
|
2012
2010
|
|
|
2011
|
+
// packages/providers/src/custom/index.ts
|
|
2012
|
+
function metadata(data) {
|
|
2013
|
+
const { origin, protocol } = data;
|
|
2014
|
+
if (typeof origin !== "string" || protocol !== "chat_completions" && protocol !== "responses") {
|
|
2015
|
+
throw new GatewayError("BAD_REQUEST", "custom credential has invalid endpoint metadata");
|
|
2016
|
+
}
|
|
2017
|
+
return { origin, protocol };
|
|
2018
|
+
}
|
|
2019
|
+
var customAdapter = {
|
|
2020
|
+
id: "custom",
|
|
2021
|
+
capabilities: PROVIDER_CAPABILITIES.custom,
|
|
2022
|
+
async send(req) {
|
|
2023
|
+
const apiKey = req.credentials.apiKey;
|
|
2024
|
+
if (apiKey === null) {
|
|
2025
|
+
throw new GatewayError("AUTH", "custom credential has no API key", { provider: "custom" });
|
|
2026
|
+
}
|
|
2027
|
+
const { origin, protocol } = metadata(req.credentials.providerData);
|
|
2028
|
+
const encoded = protocol === "chat_completions" ? toChatWire(req.request, req.model, "openai") : toResponsesWire(req.request, req.model);
|
|
2029
|
+
const headers = [
|
|
2030
|
+
["Content-Type", "application/json"],
|
|
2031
|
+
["Authorization", `Bearer ${apiKey}`]
|
|
2032
|
+
];
|
|
2033
|
+
const res = await req.http({
|
|
2034
|
+
provider: "custom",
|
|
2035
|
+
url: `${origin}/v1/${protocol === "chat_completions" ? "chat/completions" : "responses"}`,
|
|
2036
|
+
method: "POST",
|
|
2037
|
+
headers,
|
|
2038
|
+
body: JSON.stringify({ ...encoded.body, stream: true }),
|
|
2039
|
+
signal: req.signal
|
|
2040
|
+
});
|
|
2041
|
+
if (res.status < 200 || res.status >= 300)
|
|
2042
|
+
throw await httpError(res, "custom");
|
|
2043
|
+
if (res.body === null) {
|
|
2044
|
+
throw new GatewayError("UPSTREAM", "empty response body", { provider: "custom" });
|
|
2045
|
+
}
|
|
2046
|
+
return {
|
|
2047
|
+
events: protocol === "chat_completions" ? decodeChat(parseSse(res.body)) : decodeResponses(parseSse(res.body)),
|
|
2048
|
+
degradations: encoded.degradations.map((value) => value.replace(protocol === "chat_completions" ? /^kimi:/ : /^openai:/, "custom:"))
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
// packages/providers/src/http-client.ts
|
|
2053
|
+
import { request as httpRequest } from "http";
|
|
2054
|
+
import { request as httpsRequest } from "https";
|
|
2055
|
+
import { Readable } from "stream";
|
|
2056
|
+
function nodeHttpClient(options = {}) {
|
|
2057
|
+
const logger2 = options.logger ?? noopLogger;
|
|
2058
|
+
const now = options.now ?? (() => Date.now());
|
|
2059
|
+
return (req) => new Promise((resolve, reject) => {
|
|
2060
|
+
const url = new URL(req.url);
|
|
2061
|
+
const startedAt = now();
|
|
2062
|
+
let traced = false;
|
|
2063
|
+
const trace = (status, failed = false) => {
|
|
2064
|
+
if (traced || !logger2.enabled("debug"))
|
|
2065
|
+
return;
|
|
2066
|
+
traced = true;
|
|
2067
|
+
logger2.debug("upstream http", {
|
|
2068
|
+
provider: req.provider,
|
|
2069
|
+
status,
|
|
2070
|
+
host: url.host,
|
|
2071
|
+
path: url.pathname,
|
|
2072
|
+
durationMs: now() - startedAt,
|
|
2073
|
+
reason: failed ? "transport error" : undefined
|
|
2074
|
+
});
|
|
2075
|
+
};
|
|
2076
|
+
const send = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
2077
|
+
const bodyBytes = Buffer.from(req.body, "utf8");
|
|
2078
|
+
const headers = {};
|
|
2079
|
+
for (const [name, value] of req.headers)
|
|
2080
|
+
headers[name] = value;
|
|
2081
|
+
if (req.body.length > 0 && !hasHeader(req, "content-length")) {
|
|
2082
|
+
headers["Content-Length"] = bodyBytes.byteLength;
|
|
2083
|
+
}
|
|
2084
|
+
const outgoing = send({
|
|
2085
|
+
protocol: url.protocol,
|
|
2086
|
+
hostname: url.hostname,
|
|
2087
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
2088
|
+
path: `${url.pathname}${url.search}`,
|
|
2089
|
+
method: req.method,
|
|
2090
|
+
headers,
|
|
2091
|
+
setHost: !hasHeader(req, "host")
|
|
2092
|
+
}, (incoming) => {
|
|
2093
|
+
const chunks = [];
|
|
2094
|
+
let buffered = null;
|
|
2095
|
+
const responseHeaders = new Headers;
|
|
2096
|
+
for (const [k, v] of Object.entries(incoming.headers)) {
|
|
2097
|
+
if (Array.isArray(v))
|
|
2098
|
+
for (const one of v)
|
|
2099
|
+
responseHeaders.append(k, one);
|
|
2100
|
+
else if (typeof v === "string")
|
|
2101
|
+
responseHeaders.set(k, v);
|
|
2102
|
+
}
|
|
2103
|
+
trace(incoming.statusCode);
|
|
2104
|
+
resolve({
|
|
2105
|
+
status: incoming.statusCode ?? 0,
|
|
2106
|
+
headers: responseHeaders,
|
|
2107
|
+
body: Readable.toWeb(incoming),
|
|
2108
|
+
text: () => {
|
|
2109
|
+
buffered ??= new Promise((res, rej) => {
|
|
2110
|
+
incoming.on("data", (c) => chunks.push(c));
|
|
2111
|
+
incoming.on("end", () => res(Buffer.concat(chunks).toString("utf8")));
|
|
2112
|
+
incoming.on("error", rej);
|
|
2113
|
+
});
|
|
2114
|
+
return buffered;
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
});
|
|
2118
|
+
const onAbort = () => outgoing.destroy(new Error("aborted"));
|
|
2119
|
+
outgoing.on("error", (err) => {
|
|
2120
|
+
req.signal.removeEventListener("abort", onAbort);
|
|
2121
|
+
trace(undefined, true);
|
|
2122
|
+
reject(err);
|
|
2123
|
+
});
|
|
2124
|
+
outgoing.on("close", () => req.signal.removeEventListener("abort", onAbort));
|
|
2125
|
+
if (req.signal.aborted) {
|
|
2126
|
+
outgoing.destroy(new Error("aborted"));
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
req.signal.addEventListener("abort", onAbort, { once: true });
|
|
2130
|
+
if (bodyBytes.byteLength > 0)
|
|
2131
|
+
outgoing.write(bodyBytes);
|
|
2132
|
+
outgoing.end();
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
function hasHeader(req, lowerName) {
|
|
2136
|
+
return req.headers.some(([name]) => name.toLowerCase() === lowerName);
|
|
2137
|
+
}
|
|
2138
|
+
// packages/providers/src/kimi-device.ts
|
|
2139
|
+
import { randomUUID } from "crypto";
|
|
2140
|
+
function mintKimiDevice() {
|
|
2141
|
+
return {
|
|
2142
|
+
deviceId: randomUUID(),
|
|
2143
|
+
deviceName: "MacBook-Pro",
|
|
2144
|
+
deviceModel: "MacBookPro18,3",
|
|
2145
|
+
osVersion: "15.3.1"
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function kimiDeviceHeaders(providerData) {
|
|
2149
|
+
const deviceId = providerData.deviceId;
|
|
2150
|
+
if (typeof deviceId !== "string" || deviceId.length === 0)
|
|
2151
|
+
return [];
|
|
2152
|
+
const str = (v) => typeof v === "string" && v.length > 0 ? v : "unknown";
|
|
2153
|
+
return [
|
|
2154
|
+
["X-Msh-Device-Id", deviceId],
|
|
2155
|
+
["X-Msh-Device-Name", str(providerData.deviceName)],
|
|
2156
|
+
["X-Msh-Device-Model", str(providerData.deviceModel)],
|
|
2157
|
+
["X-Msh-Os-Version", str(providerData.osVersion)]
|
|
2158
|
+
];
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// packages/providers/src/kimi/index.ts
|
|
2162
|
+
var BASE_URL2 = "https://api.kimi.com/coding/v1/chat/completions";
|
|
2163
|
+
var kimiAdapter = {
|
|
2164
|
+
id: "kimi",
|
|
2165
|
+
capabilities: PROVIDER_CAPABILITIES.kimi,
|
|
2166
|
+
async send(req) {
|
|
2167
|
+
const { body, degradations } = toChatWire(req.request, req.model);
|
|
2168
|
+
const token = req.credentials.accessToken ?? req.credentials.apiKey;
|
|
2169
|
+
if (token === null) {
|
|
2170
|
+
throw new GatewayError("AUTH", "kimi credential has no token", { provider: "kimi" });
|
|
2171
|
+
}
|
|
2172
|
+
const protocol = [
|
|
2173
|
+
["Content-Type", "application/json"],
|
|
2174
|
+
["Accept", "text/event-stream"],
|
|
2175
|
+
["Authorization", `Bearer ${token}`],
|
|
2176
|
+
...kimiDeviceHeaders(req.credentials.providerData)
|
|
2177
|
+
];
|
|
2178
|
+
const profile = PROFILES.kimi;
|
|
2179
|
+
const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
|
|
2180
|
+
const res = await req.http({
|
|
2181
|
+
provider: "kimi",
|
|
2182
|
+
url: BASE_URL2,
|
|
2183
|
+
method: "POST",
|
|
2184
|
+
headers,
|
|
2185
|
+
body: JSON.stringify(orderFields({ ...body, stream: true }, BODY_ORDER.kimi)),
|
|
2186
|
+
signal: req.signal
|
|
2187
|
+
});
|
|
2188
|
+
if (res.status < 200 || res.status >= 300)
|
|
2189
|
+
throw await httpError(res, "kimi");
|
|
2190
|
+
if (res.body === null)
|
|
2191
|
+
throw new GatewayError("UPSTREAM", "empty response body", { provider: "kimi" });
|
|
2192
|
+
return { events: decodeChat(parseSse(res.body)), degradations };
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2013
2195
|
// packages/providers/src/openai/index.ts
|
|
2014
2196
|
var OAUTH_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
2015
2197
|
var API_URL = "https://api.openai.com/v1/responses";
|
|
@@ -2222,11 +2404,11 @@ function pendingError(code) {
|
|
|
2222
2404
|
error[PENDING_MARKER] = true;
|
|
2223
2405
|
return error;
|
|
2224
2406
|
}
|
|
2225
|
-
function
|
|
2407
|
+
function isRecord2(value) {
|
|
2226
2408
|
return typeof value === "object" && value !== null;
|
|
2227
2409
|
}
|
|
2228
2410
|
function recordFrom(value) {
|
|
2229
|
-
return
|
|
2411
|
+
return isRecord2(value) ? value : null;
|
|
2230
2412
|
}
|
|
2231
2413
|
function stringFrom(value, field) {
|
|
2232
2414
|
const candidate = value[field];
|
|
@@ -2429,7 +2611,7 @@ function createPendingFlows(opts) {
|
|
|
2429
2611
|
}
|
|
2430
2612
|
|
|
2431
2613
|
// packages/control/src/connect.ts
|
|
2432
|
-
var PROVIDER_IDS = ["anthropic", "openai", "kimi"];
|
|
2614
|
+
var PROVIDER_IDS = ["anthropic", "openai", "kimi", "custom"];
|
|
2433
2615
|
var FLOW_TTL_MS = 600000;
|
|
2434
2616
|
function isProviderId(value) {
|
|
2435
2617
|
return typeof value === "string" && PROVIDER_IDS.includes(value);
|
|
@@ -2470,6 +2652,8 @@ function createConnectFlows(deps) {
|
|
|
2470
2652
|
}
|
|
2471
2653
|
async function complete(flow, code) {
|
|
2472
2654
|
const provider = deps.providers[flow.provider];
|
|
2655
|
+
if (provider === undefined)
|
|
2656
|
+
throw new GatewayError("BAD_REQUEST", "provider does not support OAuth");
|
|
2473
2657
|
const result = await provider.exchange({ code, pending: flow.pending }, { http: deps.http, now: deps.now });
|
|
2474
2658
|
const id = crypto.randomUUID();
|
|
2475
2659
|
await deps.store.credentials.create({
|
|
@@ -2513,6 +2697,9 @@ function createConnectFlows(deps) {
|
|
|
2513
2697
|
}
|
|
2514
2698
|
const label = typeof labelInput === "string" && labelInput.trim().length > 0 ? labelInput.trim() : providerInput;
|
|
2515
2699
|
const provider = deps.providers[providerInput];
|
|
2700
|
+
if (provider === undefined) {
|
|
2701
|
+
throw new GatewayError("BAD_REQUEST", "provider does not support OAuth");
|
|
2702
|
+
}
|
|
2516
2703
|
const redirectUri = callbackUri(providerInput);
|
|
2517
2704
|
const start = provider.begin === undefined ? provider.start({ redirectUri }) : await (async () => {
|
|
2518
2705
|
const initial = provider.start({ redirectUri });
|
|
@@ -13765,12 +13952,12 @@ function describe(description) {
|
|
|
13765
13952
|
ch._zod.check = () => {};
|
|
13766
13953
|
return ch;
|
|
13767
13954
|
}
|
|
13768
|
-
function meta(
|
|
13955
|
+
function meta(metadata2) {
|
|
13769
13956
|
const ch = new $ZodCheck({ check: "meta" });
|
|
13770
13957
|
ch._zod.onattach = [
|
|
13771
13958
|
(inst) => {
|
|
13772
13959
|
const existing = globalRegistry.get(inst) ?? {};
|
|
13773
|
-
globalRegistry.add(inst, { ...existing, ...
|
|
13960
|
+
globalRegistry.add(inst, { ...existing, ...metadata2 });
|
|
13774
13961
|
}
|
|
13775
13962
|
];
|
|
13776
13963
|
ch._zod.check = () => {};
|
|
@@ -16902,20 +17089,15 @@ function parseOrThrow(schema, body2) {
|
|
|
16902
17089
|
const path = issue2?.path.join(".") ?? "(root)";
|
|
16903
17090
|
throw new GatewayError("BAD_REQUEST", `${path}: ${issue2?.message ?? "invalid request"}`);
|
|
16904
17091
|
}
|
|
16905
|
-
var providerIdSchema = exports_external.enum(["anthropic", "openai", "kimi"]);
|
|
17092
|
+
var providerIdSchema = exports_external.enum(["anthropic", "openai", "kimi", "custom"]);
|
|
16906
17093
|
var dryRunSchema = exports_external.object({
|
|
16907
17094
|
tools: exports_external.boolean().default(false),
|
|
16908
17095
|
images: exports_external.boolean().default(false),
|
|
16909
17096
|
reasoning: exports_external.boolean().default(false)
|
|
16910
17097
|
}).strict();
|
|
16911
|
-
var
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
}),
|
|
16915
|
-
strategy: exports_external.enum(["score", "priority", "roundRobin", "weighted"]),
|
|
16916
|
-
isAlias: exports_external.boolean(),
|
|
16917
|
-
targets: exports_external.array(exports_external.object({
|
|
16918
|
-
provider: providerIdSchema,
|
|
17098
|
+
var targetSchema = exports_external.discriminatedUnion("provider", [
|
|
17099
|
+
exports_external.object({
|
|
17100
|
+
provider: exports_external.enum(["anthropic", "openai", "kimi"]),
|
|
16919
17101
|
model: exports_external.string().min(1),
|
|
16920
17102
|
tier: exports_external.number().int().min(1),
|
|
16921
17103
|
weight: exports_external.number().positive(),
|
|
@@ -16933,7 +17115,36 @@ var modelSchema = exports_external.object({
|
|
|
16933
17115
|
images: exports_external.boolean(),
|
|
16934
17116
|
reasoning: exports_external.boolean()
|
|
16935
17117
|
})
|
|
16936
|
-
})
|
|
17118
|
+
}).strict(),
|
|
17119
|
+
exports_external.object({
|
|
17120
|
+
provider: exports_external.literal("custom"),
|
|
17121
|
+
endpointId: exports_external.string().trim().min(1),
|
|
17122
|
+
model: exports_external.string().min(1),
|
|
17123
|
+
tier: exports_external.number().int().min(1),
|
|
17124
|
+
weight: exports_external.number().positive(),
|
|
17125
|
+
costPerMTok: exports_external.object({
|
|
17126
|
+
input: exports_external.number().min(0),
|
|
17127
|
+
output: exports_external.number().min(0),
|
|
17128
|
+
cacheRead: exports_external.number().min(0).optional(),
|
|
17129
|
+
cacheWrite5m: exports_external.number().min(0).optional(),
|
|
17130
|
+
cacheWrite1h: exports_external.number().min(0).optional()
|
|
17131
|
+
}),
|
|
17132
|
+
contextWindow: exports_external.number().int().positive().optional(),
|
|
17133
|
+
maxOutputTokens: exports_external.number().int().positive().optional(),
|
|
17134
|
+
capabilities: exports_external.object({
|
|
17135
|
+
tools: exports_external.boolean(),
|
|
17136
|
+
images: exports_external.boolean(),
|
|
17137
|
+
reasoning: exports_external.boolean()
|
|
17138
|
+
})
|
|
17139
|
+
}).strict()
|
|
17140
|
+
]);
|
|
17141
|
+
var modelSchema = exports_external.object({
|
|
17142
|
+
id: exports_external.string().min(1).refine((value) => !value.toLowerCase().startsWith("claude/"), {
|
|
17143
|
+
message: 'model id must not start with "claude/": that prefix is reserved for discovery mirrors'
|
|
17144
|
+
}),
|
|
17145
|
+
strategy: exports_external.enum(["score", "priority", "roundRobin", "weighted"]),
|
|
17146
|
+
isAlias: exports_external.boolean(),
|
|
17147
|
+
targets: exports_external.array(targetSchema).min(1, "a virtual model needs at least one target")
|
|
16937
17148
|
});
|
|
16938
17149
|
var keyCreateSchema = exports_external.object({
|
|
16939
17150
|
label: exports_external.string().min(1).default("api key"),
|
|
@@ -16947,7 +17158,7 @@ var settingsSchema = exports_external.object({
|
|
|
16947
17158
|
quota: exports_external.number(),
|
|
16948
17159
|
cost: exports_external.number(),
|
|
16949
17160
|
latency: exports_external.number(),
|
|
16950
|
-
|
|
17161
|
+
load: exports_external.number()
|
|
16951
17162
|
}).strict(),
|
|
16952
17163
|
maxAttempts: exports_external.number().int().min(1).max(10),
|
|
16953
17164
|
requestDeadlineMs: exports_external.number().int().min(0),
|
|
@@ -17012,14 +17223,48 @@ async function getCredential(store, id) {
|
|
|
17012
17223
|
throw new GatewayError("BAD_REQUEST", "no such credential");
|
|
17013
17224
|
return summarizeCredential(credential);
|
|
17014
17225
|
}
|
|
17226
|
+
function requiredString(value, field) {
|
|
17227
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
17228
|
+
throw new GatewayError("BAD_REQUEST", `${field}: must not be empty`);
|
|
17229
|
+
}
|
|
17230
|
+
return value.trim();
|
|
17231
|
+
}
|
|
17232
|
+
function customProviderData(input) {
|
|
17233
|
+
const endpointId = requiredString(input.endpointId, "endpointId");
|
|
17234
|
+
const endpointLabel = requiredString(input.endpointLabel, "endpointLabel");
|
|
17235
|
+
const originInput = requiredString(input.origin, "origin");
|
|
17236
|
+
if (input.protocol !== "chat_completions" && input.protocol !== "responses") {
|
|
17237
|
+
throw new GatewayError("BAD_REQUEST", "protocol: unsupported protocol");
|
|
17238
|
+
}
|
|
17239
|
+
let url2;
|
|
17240
|
+
try {
|
|
17241
|
+
url2 = new URL(originInput);
|
|
17242
|
+
} catch {
|
|
17243
|
+
throw new GatewayError("BAD_REQUEST", "origin: must be a valid URL");
|
|
17244
|
+
}
|
|
17245
|
+
if (url2.protocol !== "http:" && url2.protocol !== "https:" || url2.hostname.length === 0 || url2.username.length > 0 || url2.password.length > 0 || url2.pathname !== "" && url2.pathname !== "/" || url2.search.length > 0 || url2.hash.length > 0) {
|
|
17246
|
+
throw new GatewayError("BAD_REQUEST", "origin: must be an HTTP(S) server origin");
|
|
17247
|
+
}
|
|
17248
|
+
return { endpointId, endpointLabel, origin: url2.origin, protocol: input.protocol };
|
|
17249
|
+
}
|
|
17250
|
+
function sameCustomEndpoint(a, b) {
|
|
17251
|
+
return a.endpointId === b.endpointId && a.endpointLabel === b.endpointLabel && a.origin === b.origin && a.protocol === b.protocol;
|
|
17252
|
+
}
|
|
17015
17253
|
async function createApiKeyCredential(store, input, logger2 = noopLogger) {
|
|
17016
17254
|
const provider = parseOrThrow(providerIdSchema, input.provider);
|
|
17017
|
-
|
|
17018
|
-
throw new GatewayError("BAD_REQUEST", "apiKey: must not be empty");
|
|
17019
|
-
}
|
|
17255
|
+
const apiKey = requiredString(input.apiKey, "apiKey");
|
|
17020
17256
|
if (input.label !== undefined && typeof input.label !== "string") {
|
|
17021
17257
|
throw new GatewayError("BAD_REQUEST", "label: must be a string");
|
|
17022
17258
|
}
|
|
17259
|
+
let providerData = {};
|
|
17260
|
+
if (provider === "custom") {
|
|
17261
|
+
const custom2 = customProviderData(input);
|
|
17262
|
+
const existing = (await store.credentials.list()).filter((credential) => credential.provider === "custom" && credential.providerData.endpointId === custom2.endpointId);
|
|
17263
|
+
if (existing.some((credential) => !sameCustomEndpoint(credential.providerData, custom2))) {
|
|
17264
|
+
throw new GatewayError("CONFLICT", `endpointId: metadata conflicts with existing endpoint`);
|
|
17265
|
+
}
|
|
17266
|
+
providerData = custom2;
|
|
17267
|
+
}
|
|
17023
17268
|
const label = input.label?.trim() || `${provider} api key`;
|
|
17024
17269
|
const created = await store.credentials.create({
|
|
17025
17270
|
id: crypto.randomUUID(),
|
|
@@ -17031,12 +17276,12 @@ async function createApiKeyCredential(store, input, logger2 = noopLogger) {
|
|
|
17031
17276
|
weight: 1,
|
|
17032
17277
|
expiresAt: null,
|
|
17033
17278
|
accountEmail: null,
|
|
17034
|
-
providerData
|
|
17279
|
+
providerData,
|
|
17035
17280
|
disabledReason: null,
|
|
17036
17281
|
disabledAt: null,
|
|
17037
17282
|
accessToken: null,
|
|
17038
17283
|
refreshToken: null,
|
|
17039
|
-
apiKey
|
|
17284
|
+
apiKey,
|
|
17040
17285
|
idToken: null
|
|
17041
17286
|
});
|
|
17042
17287
|
logger2.info("credential added", { credentialId: created.id, provider: created.provider });
|
|
@@ -17171,6 +17416,9 @@ function eligible(input) {
|
|
|
17171
17416
|
for (const credential of snapshot.credentials) {
|
|
17172
17417
|
if (credential.provider !== target.provider)
|
|
17173
17418
|
continue;
|
|
17419
|
+
if (target.provider === "custom" && credential.providerData.endpointId !== target.endpointId) {
|
|
17420
|
+
continue;
|
|
17421
|
+
}
|
|
17174
17422
|
const drop = (reason) => {
|
|
17175
17423
|
excluded.push({ credentialId: credential.id, model: target.model, reason });
|
|
17176
17424
|
};
|
|
@@ -17242,6 +17490,22 @@ function quotaHeadroom(credential, windows, now, pollIntervalMs) {
|
|
|
17242
17490
|
return Math.min(...usable.map((w) => paceAdjusted(w, now)));
|
|
17243
17491
|
}
|
|
17244
17492
|
|
|
17493
|
+
// packages/store/src/types.ts
|
|
17494
|
+
var READ_OVER_INPUT = 0.1;
|
|
17495
|
+
function cacheReadRate(prices) {
|
|
17496
|
+
return prices.cacheRead ?? prices.input * READ_OVER_INPUT;
|
|
17497
|
+
}
|
|
17498
|
+
var DEFAULT_SETTINGS = {
|
|
17499
|
+
weights: { tier: 10, health: 3, quota: 2, load: 2, cost: 1, latency: 1 },
|
|
17500
|
+
maxAttempts: 3,
|
|
17501
|
+
requestDeadlineMs: 120000,
|
|
17502
|
+
breakerThreshold: 3,
|
|
17503
|
+
breakerCooldownMs: 30000,
|
|
17504
|
+
logRetentionDays: 30,
|
|
17505
|
+
quotaPollIntervalMs: 300000,
|
|
17506
|
+
rtkEnabled: false
|
|
17507
|
+
};
|
|
17508
|
+
|
|
17245
17509
|
// packages/router/src/score.ts
|
|
17246
17510
|
var UNKNOWN = 0.5;
|
|
17247
17511
|
function lowerIsBetter(value, min, max) {
|
|
@@ -17249,43 +17513,49 @@ function lowerIsBetter(value, min, max) {
|
|
|
17249
17513
|
return 1;
|
|
17250
17514
|
return (max - value) / (max - min);
|
|
17251
17515
|
}
|
|
17252
|
-
function
|
|
17253
|
-
return
|
|
17516
|
+
function ratio(value, min) {
|
|
17517
|
+
return Math.min(1, min / value);
|
|
17518
|
+
}
|
|
17519
|
+
function bestPositive(values) {
|
|
17520
|
+
const positive = values.filter((v) => v > 0);
|
|
17521
|
+
return positive.length > 0 ? Math.min(...positive) : null;
|
|
17522
|
+
}
|
|
17523
|
+
var EXPECTED_OUTPUT_TOKENS = 1000;
|
|
17524
|
+
function requestCost(target, request2) {
|
|
17525
|
+
const cachedTok = estimateCachedInputTokens(request2);
|
|
17526
|
+
const freshTok = Math.max(0, estimateInputTokens(request2) - cachedTok);
|
|
17527
|
+
const outTok = Math.min(request2.maxTokens ?? EXPECTED_OUTPUT_TOKENS, EXPECTED_OUTPUT_TOKENS);
|
|
17528
|
+
return freshTok * target.costPerMTok.input + cachedTok * cacheReadRate(target.costPerMTok) + outTok * target.costPerMTok.output;
|
|
17529
|
+
}
|
|
17530
|
+
function healthScore(h) {
|
|
17531
|
+
const base = 1 / (1 + (h?.consecutiveFailures ?? 0));
|
|
17532
|
+
return h?.breakerState === "open" || h?.breakerState === "halfOpen" ? base * 0.5 : base;
|
|
17254
17533
|
}
|
|
17255
17534
|
function score(pairs, input) {
|
|
17256
|
-
const { snapshot, now } = input;
|
|
17535
|
+
const { snapshot, now, load } = input;
|
|
17257
17536
|
const w = snapshot.settings.weights;
|
|
17258
17537
|
const tiers = pairs.map((p) => p.target.tier);
|
|
17259
17538
|
const minTier = Math.min(...tiers);
|
|
17260
17539
|
const maxTier = Math.max(...tiers);
|
|
17261
|
-
const costs = pairs.map((p) =>
|
|
17262
|
-
const
|
|
17263
|
-
const
|
|
17264
|
-
const latencies = pairs.flatMap((p) => {
|
|
17540
|
+
const costs = pairs.map((p) => requestCost(p.target, input.request));
|
|
17541
|
+
const bestCost = bestPositive(costs);
|
|
17542
|
+
const bestLatency = bestPositive(pairs.flatMap((p) => {
|
|
17265
17543
|
const h = snapshot.health.get(healthKey(p.credential.id, p.target.model));
|
|
17266
17544
|
return h?.ewmaTtftMs != null ? [h.ewmaTtftMs] : [];
|
|
17267
|
-
});
|
|
17268
|
-
const minLatency = latencies.length > 0 ? Math.min(...latencies) : 0;
|
|
17269
|
-
const maxLatency = latencies.length > 0 ? Math.max(...latencies) : 0;
|
|
17270
|
-
const idleTimes = pairs.map((p) => {
|
|
17271
|
-
const h = snapshot.health.get(healthKey(p.credential.id, p.target.model));
|
|
17272
|
-
return h?.lastUsedAt == null ? Number.POSITIVE_INFINITY : now - h.lastUsedAt;
|
|
17273
|
-
});
|
|
17274
|
-
const finiteIdle = idleTimes.filter(Number.isFinite);
|
|
17275
|
-
const maxIdle = finiteIdle.length > 0 ? Math.max(...finiteIdle) : 1;
|
|
17545
|
+
}));
|
|
17276
17546
|
return pairs.map((pair, i) => {
|
|
17277
|
-
const
|
|
17547
|
+
const key = healthKey(pair.credential.id, pair.target.model);
|
|
17548
|
+
const h = snapshot.health.get(key);
|
|
17278
17549
|
const tier = lowerIsBetter(pair.target.tier, minTier, maxTier);
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17550
|
+
const inflight = load.get(key) ?? 0;
|
|
17551
|
+
const loadTerm = 1 / (1 + inflight);
|
|
17552
|
+
const health = healthScore(h);
|
|
17282
17553
|
const quota = quotaHeadroom(pair.credential, snapshot.quota.get(pair.credential.id) ?? [], now, snapshot.settings.quotaPollIntervalMs);
|
|
17283
|
-
const
|
|
17284
|
-
const
|
|
17285
|
-
const
|
|
17286
|
-
const
|
|
17287
|
-
const
|
|
17288
|
-
const base = tier * w.tier + health * w.health + quota * w.quota + cost * w.cost + latency * w.latency + recency * w.recency;
|
|
17554
|
+
const ownCost = costs[i];
|
|
17555
|
+
const cost = ownCost <= 0 || bestCost === null ? UNKNOWN : ratio(ownCost, bestCost);
|
|
17556
|
+
const latency = h?.ewmaTtftMs == null || h.ewmaTtftMs <= 0 || bestLatency === null ? UNKNOWN : ratio(h.ewmaTtftMs, bestLatency);
|
|
17557
|
+
const reasons = { tier, health, quota, cost, latency, load: loadTerm };
|
|
17558
|
+
const base = tier * w.tier + health * w.health + quota * w.quota + cost * w.cost + latency * w.latency + loadTerm * w.load;
|
|
17289
17559
|
return {
|
|
17290
17560
|
credential: pair.credential,
|
|
17291
17561
|
target: pair.target,
|
|
@@ -17298,8 +17568,8 @@ function score(pairs, input) {
|
|
|
17298
17568
|
var PROVIDERS = new Set(Object.keys(PROVIDER_CAPABILITIES));
|
|
17299
17569
|
|
|
17300
17570
|
// packages/router/src/index.ts
|
|
17301
|
-
function weightedShuffle(candidates, rand, headroom) {
|
|
17302
|
-
const drawWeight = (c) => c.credential.weight * c.target.weight * headroom(c);
|
|
17571
|
+
function weightedShuffle(candidates, rand, headroom, health) {
|
|
17572
|
+
const drawWeight = (c) => c.credential.weight * c.target.weight * headroom(c) * health(c);
|
|
17303
17573
|
const total = candidates.reduce((sum, c) => sum + drawWeight(c), 0);
|
|
17304
17574
|
if (total <= 0)
|
|
17305
17575
|
return [...candidates].sort((a, b) => b.score - a.score);
|
|
@@ -17322,6 +17592,8 @@ function rank(input) {
|
|
|
17322
17592
|
return { candidates: [], excluded };
|
|
17323
17593
|
const scored = score(pairs, input);
|
|
17324
17594
|
const headroom = (c) => quotaHeadroom(c.credential, input.snapshot.quota.get(c.credential.id) ?? [], input.now, input.snapshot.settings.quotaPollIntervalMs);
|
|
17595
|
+
const health = (c) => healthScore(input.snapshot.health.get(healthKey(c.credential.id, c.target.model)));
|
|
17596
|
+
const inflight = (c) => input.load.get(healthKey(c.credential.id, c.target.model)) ?? 0;
|
|
17325
17597
|
switch (input.model.strategy) {
|
|
17326
17598
|
case "priority":
|
|
17327
17599
|
scored.sort((a, b) => a.target.tier - b.target.tier || b.score - a.score);
|
|
@@ -17332,11 +17604,11 @@ function rank(input) {
|
|
|
17332
17604
|
return h?.lastUsedAt == null ? Number.POSITIVE_INFINITY : input.now - h.lastUsedAt;
|
|
17333
17605
|
};
|
|
17334
17606
|
const spent = (c) => headroom(c) < QUOTA_FLOOR ? 1 : 0;
|
|
17335
|
-
scored.sort((a, b) => spent(a) - spent(b) || idle(b) - idle(a));
|
|
17607
|
+
scored.sort((a, b) => spent(a) - spent(b) || inflight(a) - inflight(b) || idle(b) - idle(a));
|
|
17336
17608
|
break;
|
|
17337
17609
|
}
|
|
17338
17610
|
case "weighted":
|
|
17339
|
-
return { candidates: weightedShuffle(scored, input.rand, headroom), excluded };
|
|
17611
|
+
return { candidates: weightedShuffle(scored, input.rand, headroom, health), excluded };
|
|
17340
17612
|
case "score":
|
|
17341
17613
|
scored.sort((a, b) => b.score - a.score);
|
|
17342
17614
|
break;
|
|
@@ -17369,7 +17641,7 @@ async function dryRun(deps, modelId, input) {
|
|
|
17369
17641
|
} : {},
|
|
17370
17642
|
...need.reasoning ? { reasoning: { mode: "adaptive" } } : {}
|
|
17371
17643
|
};
|
|
17372
|
-
const result = rank({ request: probe, model, snapshot, now, rand: 0 });
|
|
17644
|
+
const result = rank({ request: probe, model, snapshot, now, rand: 0, load: new Map });
|
|
17373
17645
|
return {
|
|
17374
17646
|
modelId: model.id,
|
|
17375
17647
|
strategy: model.strategy,
|
|
@@ -17426,21 +17698,24 @@ function unhex(s) {
|
|
|
17426
17698
|
out[i] = Number.parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
17427
17699
|
return out;
|
|
17428
17700
|
}
|
|
17429
|
-
// packages/store/src/types.ts
|
|
17430
|
-
var DEFAULT_SETTINGS = {
|
|
17431
|
-
weights: { tier: 10, health: 3, quota: 2, cost: 1, latency: 1, recency: 0.5 },
|
|
17432
|
-
maxAttempts: 3,
|
|
17433
|
-
requestDeadlineMs: 120000,
|
|
17434
|
-
breakerThreshold: 3,
|
|
17435
|
-
breakerCooldownMs: 30000,
|
|
17436
|
-
logRetentionDays: 30,
|
|
17437
|
-
quotaPollIntervalMs: 300000,
|
|
17438
|
-
rtkEnabled: false
|
|
17439
|
-
};
|
|
17440
|
-
|
|
17441
17701
|
// packages/store/src/sqlite/config.ts
|
|
17442
17702
|
var SETTINGS_KEY = "settings";
|
|
17443
17703
|
var ADMIN_HASH_KEY = "adminPasswordHash";
|
|
17704
|
+
function knownWeights(stored) {
|
|
17705
|
+
const d = DEFAULT_SETTINGS.weights;
|
|
17706
|
+
const pick2 = (key) => {
|
|
17707
|
+
const value = stored?.[key];
|
|
17708
|
+
return typeof value === "number" && Number.isFinite(value) ? value : d[key];
|
|
17709
|
+
};
|
|
17710
|
+
return {
|
|
17711
|
+
tier: pick2("tier"),
|
|
17712
|
+
health: pick2("health"),
|
|
17713
|
+
quota: pick2("quota"),
|
|
17714
|
+
load: pick2("load"),
|
|
17715
|
+
cost: pick2("cost"),
|
|
17716
|
+
latency: pick2("latency")
|
|
17717
|
+
};
|
|
17718
|
+
}
|
|
17444
17719
|
function createConfigRepo(db, emit2 = () => {}) {
|
|
17445
17720
|
const readRaw = (key) => db.query("SELECT value FROM settings WHERE key = ?").get(key)?.value ?? null;
|
|
17446
17721
|
const writeRaw = (key, value) => {
|
|
@@ -17478,7 +17753,7 @@ function createConfigRepo(db, emit2 = () => {}) {
|
|
|
17478
17753
|
...DEFAULT_SETTINGS,
|
|
17479
17754
|
...stored,
|
|
17480
17755
|
rtkEnabled: stored.rtkEnabled === true,
|
|
17481
|
-
weights:
|
|
17756
|
+
weights: knownWeights(stored.weights)
|
|
17482
17757
|
};
|
|
17483
17758
|
} catch {
|
|
17484
17759
|
return DEFAULT_SETTINGS;
|
|
@@ -18515,6 +18790,11 @@ async function putModel(store, id, input) {
|
|
|
18515
18790
|
if (model.id !== id) {
|
|
18516
18791
|
throw new GatewayError("BAD_REQUEST", "model id in the path and body must match");
|
|
18517
18792
|
}
|
|
18793
|
+
const customEndpointIds = new Set((await store.credentials.list()).filter((credential) => credential.provider === "custom").map((credential) => credential.providerData.endpointId).filter((endpointId) => typeof endpointId === "string"));
|
|
18794
|
+
const missing = model.targets.find((target) => target.provider === "custom" && (target.endpointId === undefined || !customEndpointIds.has(target.endpointId)));
|
|
18795
|
+
if (missing !== undefined) {
|
|
18796
|
+
throw new GatewayError("BAD_REQUEST", `custom endpoint "${missing.endpointId}" has no credential`);
|
|
18797
|
+
}
|
|
18518
18798
|
await store.config.putModel(model);
|
|
18519
18799
|
}
|
|
18520
18800
|
async function removeModel(store, id) {
|
|
@@ -18637,7 +18917,7 @@ var AUTHORIZE_URL2 = "https://auth.openai.com/oauth/authorize";
|
|
|
18637
18917
|
var TOKEN_URL3 = "https://auth.openai.com/oauth/token";
|
|
18638
18918
|
var SCOPES2 = "openid profile email offline_access";
|
|
18639
18919
|
var USAGE_URL3 = "https://chatgpt.com/backend-api/wham/usage";
|
|
18640
|
-
function
|
|
18920
|
+
function isRecord3(value) {
|
|
18641
18921
|
return typeof value === "object" && value !== null;
|
|
18642
18922
|
}
|
|
18643
18923
|
function nonBlankStringOrNull(value) {
|
|
@@ -18654,19 +18934,19 @@ function decodeClaims(idToken) {
|
|
|
18654
18934
|
}
|
|
18655
18935
|
try {
|
|
18656
18936
|
const json5 = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
18657
|
-
if (!
|
|
18937
|
+
if (!isRecord3(json5))
|
|
18658
18938
|
return { email: null, accountId: null };
|
|
18659
18939
|
const auth = json5["https://api.openai.com/auth"];
|
|
18660
18940
|
return {
|
|
18661
18941
|
email: typeof json5.email === "string" ? json5.email : null,
|
|
18662
|
-
accountId:
|
|
18942
|
+
accountId: isRecord3(auth) ? nonBlankStringOrNull(auth.chatgpt_account_id) : null
|
|
18663
18943
|
};
|
|
18664
18944
|
} catch {
|
|
18665
18945
|
return { email: null, accountId: null };
|
|
18666
18946
|
}
|
|
18667
18947
|
}
|
|
18668
18948
|
function parseTokenResponse(value) {
|
|
18669
|
-
if (!
|
|
18949
|
+
if (!isRecord3(value) || typeof value.access_token !== "string")
|
|
18670
18950
|
return null;
|
|
18671
18951
|
return {
|
|
18672
18952
|
accessToken: value.access_token,
|
|
@@ -18801,6 +19081,9 @@ function createRefresher(deps) {
|
|
|
18801
19081
|
throw new GatewayError("AUTH", `credential ${credential.id} has no refresh token`);
|
|
18802
19082
|
}
|
|
18803
19083
|
const provider = deps.providers[credential.provider];
|
|
19084
|
+
if (provider === undefined) {
|
|
19085
|
+
throw new GatewayError("BAD_REQUEST", "provider does not support OAuth refresh");
|
|
19086
|
+
}
|
|
18804
19087
|
logger2.debug("refreshing credential", {
|
|
18805
19088
|
provider: credential.provider,
|
|
18806
19089
|
credentialId: credential.id
|
|
@@ -19520,7 +19803,8 @@ var console_ = {
|
|
|
19520
19803
|
var PROVIDER_TONE = {
|
|
19521
19804
|
anthropic: "magenta",
|
|
19522
19805
|
openai: "green",
|
|
19523
|
-
kimi: "blue"
|
|
19806
|
+
kimi: "blue",
|
|
19807
|
+
custom: "cyan"
|
|
19524
19808
|
};
|
|
19525
19809
|
function provider(ctx, id) {
|
|
19526
19810
|
return paint(ctx, PROVIDER_TONE[id], id);
|
|
@@ -19684,21 +19968,36 @@ var credentialsRefresh = {
|
|
|
19684
19968
|
}
|
|
19685
19969
|
};
|
|
19686
19970
|
var credentialsAddKey = {
|
|
19687
|
-
usage: "credentials add-key <provider> [--label L]",
|
|
19971
|
+
usage: "credentials add-key <provider> [--label L] [--endpoint-id ID --endpoint-label L --origin URL --protocol P]",
|
|
19688
19972
|
summary: "Store a provider API key, read from a prompt or stdin",
|
|
19689
|
-
options: {
|
|
19973
|
+
options: {
|
|
19974
|
+
label: { type: "string" },
|
|
19975
|
+
"endpoint-id": { type: "string" },
|
|
19976
|
+
"endpoint-label": { type: "string" },
|
|
19977
|
+
origin: { type: "string" },
|
|
19978
|
+
protocol: { type: "string" }
|
|
19979
|
+
},
|
|
19690
19980
|
async run(args, { ctx, writer, prompt }) {
|
|
19691
19981
|
const providerId = requirePositional(args, 0, "provider");
|
|
19692
19982
|
if (!isProviderId(providerId)) {
|
|
19693
|
-
throw new UsageError("provider must be one of anthropic, openai, kimi");
|
|
19983
|
+
throw new UsageError("provider must be one of anthropic, openai, kimi, custom");
|
|
19694
19984
|
}
|
|
19985
|
+
const protocolFlag = stringFlag(args.values, "protocol");
|
|
19986
|
+
const protocol = protocolFlag === "chat-completions" ? "chat_completions" : protocolFlag === "responses" ? "responses" : protocolFlag;
|
|
19695
19987
|
const key = await prompt.secret(`${providerId} API key: `);
|
|
19696
19988
|
if (key.length === 0)
|
|
19697
19989
|
throw new CliError("no API key given");
|
|
19698
|
-
const
|
|
19990
|
+
const store = await ctx.store();
|
|
19991
|
+
const endpointId = stringFlag(args.values, "endpoint-id");
|
|
19992
|
+
const existingEndpoint = providerId === "custom" && endpointId !== undefined ? (await listCredentials(store)).find((credential) => credential.provider === "custom" && credential.providerData.endpointId === endpointId.trim()) : undefined;
|
|
19993
|
+
const created = await createApiKeyCredential(store, {
|
|
19699
19994
|
provider: providerId,
|
|
19700
19995
|
apiKey: key,
|
|
19701
|
-
label: stringFlag(args.values, "label")
|
|
19996
|
+
label: stringFlag(args.values, "label"),
|
|
19997
|
+
endpointId,
|
|
19998
|
+
endpointLabel: stringFlag(args.values, "endpoint-label") ?? existingEndpoint?.providerData.endpointLabel,
|
|
19999
|
+
origin: stringFlag(args.values, "origin") ?? existingEndpoint?.providerData.origin,
|
|
20000
|
+
protocol: protocol ?? existingEndpoint?.providerData.protocol
|
|
19702
20001
|
});
|
|
19703
20002
|
emit(ctx, writer, { id: created.id, provider: created.provider }, () => `stored ${created.provider} api key as ${created.id}`);
|
|
19704
20003
|
}
|