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.
Files changed (42) hide show
  1. package/bin/omni.js +527 -228
  2. package/gateway.js +763 -401
  3. package/package.json +1 -1
  4. package/public/assets/{Chip-BB_5C1Zp.js → Chip-C4X8tf5z.js} +1 -1
  5. package/public/assets/Confirm-BnmH8Gn0.js +4 -0
  6. package/public/assets/{CopyValue-CRQDLo7k.js → CopyValue-B-uR4Js-.js} +5 -5
  7. package/public/assets/{Field-uHZxl4fI.js → Field-Ct4SeDZD.js} +2 -2
  8. package/public/assets/{Lamp-B-5SjXbG.js → Lamp-CBOwqG5K.js} +7 -7
  9. package/public/assets/{Meter-DI_BRUKt.js → Meter-COdz0dwY.js} +1 -1
  10. package/public/assets/{Modal-CI6jk2D4.js → Modal-DMFUHQ-A.js} +8 -8
  11. package/public/assets/{Rack-D1WJswv3.js → Rack-DAbY9x1I.js} +18 -18
  12. package/public/assets/{Readout-BocZ2HXP.js → Readout-Bl2kD9Tl.js} +6 -2
  13. package/public/assets/{States-Bbiu5cHE.js → States-D5Memn8D.js} +4 -4
  14. package/public/assets/{Table-CdPWxYaz.js → Table-DObRatbW.js} +1 -1
  15. package/public/assets/{Toggle-CiLC67Dw.js → Toggle-Dkjlvck5.js} +1 -1
  16. package/public/assets/{TokenBreakdown-B96iPBm9.js → TokenBreakdown-oXP49ZTa.js} +5 -2
  17. package/public/assets/_app-Brc2wQZm.js +1 -0
  18. package/public/assets/_app.accounts-I9RXUiyi.js +54 -0
  19. package/public/assets/{_app.console-Daz4aKhf.js → _app.console-BVVpryIB.js} +10 -10
  20. package/public/assets/_app.index-83Ig4FUL.js +62 -0
  21. package/public/assets/_app.keys-KeP6L4g_.js +39 -0
  22. package/public/assets/_app.logs-B5oh01Ls.js +32 -0
  23. package/public/assets/_app.models-BuOFMZUN.js +144 -0
  24. package/public/assets/{_app.settings-a0FKyQAi.js → _app.settings-BN436vI2.js} +4 -4
  25. package/public/assets/_app.usage-C528VOia.js +166 -0
  26. package/public/assets/catalog-kJ53n_fc.js +1 -0
  27. package/public/assets/{dist-C-IbPRiV.js → dist-CnzT-Ut2.js} +1 -1
  28. package/public/assets/index-CE-KQ-ju.js +172 -0
  29. package/public/assets/{login-CTvH_KAd.js → login-2LpCZOtB.js} +8 -8
  30. package/public/assets/{queries-D2o-X8Pj.js → queries-1zRLkX-Q.js} +27 -27
  31. package/public/assets/{trash-2-BcZb-sCT.js → trash-2-Da6cLgPn.js} +1 -1
  32. package/public/index.html +2 -2
  33. package/public/assets/Confirm-B6aAiVbT.js +0 -4
  34. package/public/assets/_app-BBOF6A0T.js +0 -1
  35. package/public/assets/_app.accounts-BNkhpvaB.js +0 -54
  36. package/public/assets/_app.index-LO6d38oe.js +0 -62
  37. package/public/assets/_app.keys-kBqLqoFf.js +0 -39
  38. package/public/assets/_app.logs-_wYNS47N.js +0 -32
  39. package/public/assets/_app.models-Cjng8ohC.js +0 -144
  40. package/public/assets/_app.usage-D3KtgLrC.js +0 -166
  41. package/public/assets/catalog-C_OQ0icG.js +0 -1
  42. 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 = { ...body.output_config ?? {}, effort: req.reasoning.effort };
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
- Object.assign(body, req.vendor?.anthropic ?? {});
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?.kimi ?? {});
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 isRecord(value) {
2407
+ function isRecord2(value) {
2226
2408
  return typeof value === "object" && value !== null;
2227
2409
  }
2228
2410
  function recordFrom(value) {
2229
- return isRecord(value) ? value : null;
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(metadata) {
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, ...metadata });
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 modelSchema = exports_external.object({
16912
- id: exports_external.string().min(1).refine((value) => !value.toLowerCase().startsWith("claude/"), {
16913
- message: 'model id must not start with "claude/": that prefix is reserved for discovery mirrors'
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
- })).min(1, "a virtual model needs at least one target")
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
- recency: exports_external.number()
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
- if (typeof input.apiKey !== "string" || input.apiKey.trim().length === 0) {
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: input.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 blendedCost(input, output) {
17253
- return input * 0.25 + output * 0.75;
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) => blendedCost(p.target.costPerMTok.input, p.target.costPerMTok.output));
17262
- const minCost = Math.min(...costs);
17263
- const maxCost = Math.max(...costs);
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 h = snapshot.health.get(healthKey(pair.credential.id, pair.target.model));
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
- let health = 1 / (1 + (h?.consecutiveFailures ?? 0));
17280
- if (h?.breakerState === "open" || h?.breakerState === "halfOpen")
17281
- health *= 0.5;
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 cost = maxCost === 0 ? UNKNOWN : lowerIsBetter(costs[i], minCost, maxCost);
17284
- const latency = h?.ewmaTtftMs == null ? UNKNOWN : lowerIsBetter(h.ewmaTtftMs, minLatency, maxLatency);
17285
- const idle = idleTimes[i];
17286
- const recency = Number.isFinite(idle) ? Math.min(1, idle / (maxIdle || 1)) : 1;
17287
- const reasons = { tier, health, quota, cost, latency, recency };
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: { ...DEFAULT_SETTINGS.weights, ...stored.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 isRecord2(value) {
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 (!isRecord2(json5))
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: isRecord2(auth) ? nonBlankStringOrNull(auth.chatgpt_account_id) : null
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 (!isRecord2(value) || typeof value.access_token !== "string")
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: { label: { type: "string" } },
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 created = await createApiKeyCredential(await ctx.store(), {
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
  }