omnigateway 0.1.8 → 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 (48) hide show
  1. package/README.md +27 -33
  2. package/bin/omni.js +1133 -305
  3. package/gateway.js +3632 -543
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-CM9ZMdr9.js → Chip-C4X8tf5z.js} +1 -1
  6. package/public/assets/Confirm-BnmH8Gn0.js +4 -0
  7. package/public/assets/{CopyValue-WrOTcHwq.js → CopyValue-B-uR4Js-.js} +5 -5
  8. package/public/assets/{Field-Br6jKPz8.js → Field-Ct4SeDZD.js} +2 -2
  9. package/public/assets/Lamp-CBOwqG5K.js +25 -0
  10. package/public/assets/{Meter-BzMaum0C.js → Meter-COdz0dwY.js} +1 -1
  11. package/public/assets/Modal-DMFUHQ-A.js +82 -0
  12. package/public/assets/{Rack-BFWC50ex.js → Rack-DAbY9x1I.js} +23 -19
  13. package/public/assets/{Readout-DMJs4gYD.js → Readout-Bl2kD9Tl.js} +6 -2
  14. package/public/assets/{States-BNeCLhZn.js → States-D5Memn8D.js} +4 -4
  15. package/public/assets/{Table-nOg_lgEJ.js → Table-DObRatbW.js} +1 -1
  16. package/public/assets/Toggle-Dkjlvck5.js +39 -0
  17. package/public/assets/TokenBreakdown-oXP49ZTa.js +31 -0
  18. package/public/assets/_app-Brc2wQZm.js +1 -0
  19. package/public/assets/_app.accounts-I9RXUiyi.js +54 -0
  20. package/public/assets/_app.console-BVVpryIB.js +44 -0
  21. package/public/assets/_app.index-83Ig4FUL.js +62 -0
  22. package/public/assets/_app.keys-KeP6L4g_.js +39 -0
  23. package/public/assets/_app.logs-B5oh01Ls.js +32 -0
  24. package/public/assets/_app.models-BuOFMZUN.js +144 -0
  25. package/public/assets/_app.settings-BN436vI2.js +38 -0
  26. package/public/assets/_app.usage-C528VOia.js +166 -0
  27. package/public/assets/catalog-kJ53n_fc.js +1 -0
  28. package/public/assets/dist-CnzT-Ut2.js +1 -0
  29. package/public/assets/index-CE-KQ-ju.js +172 -0
  30. package/public/assets/{login-BpUl6C5b.js → login-2LpCZOtB.js} +8 -8
  31. package/public/assets/queries-1zRLkX-Q.js +144 -0
  32. package/public/assets/{trash-2-DjWP2u19.js → trash-2-Da6cLgPn.js} +1 -1
  33. package/public/index.html +2 -2
  34. package/public/assets/Lamp-DO-sRDg2.js +0 -25
  35. package/public/assets/Modal-lhjr949H.js +0 -82
  36. package/public/assets/Toggle-DYoCVD37.js +0 -42
  37. package/public/assets/_app-lXr06Xli.js +0 -1
  38. package/public/assets/_app.accounts-BTb5vHI_.js +0 -51
  39. package/public/assets/_app.console-DHUClMsI.js +0 -25
  40. package/public/assets/_app.index-DZ--9mKU.js +0 -61
  41. package/public/assets/_app.keys-B-cy-2DS.js +0 -39
  42. package/public/assets/_app.logs-BQjAIT2P.js +0 -33
  43. package/public/assets/_app.models-BAmXHlf2.js +0 -144
  44. package/public/assets/_app.settings-CG8bv_rm.js +0 -34
  45. package/public/assets/_app.usage-BLrQ2hB8.js +0 -163
  46. package/public/assets/catalog-C_OQ0icG.js +0 -1
  47. package/public/assets/index-UAZ0O5y0.js +0 -170
  48. package/public/assets/queries-D_-Jj9vt.js +0 -144
package/bin/omni.js CHANGED
@@ -91,9 +91,8 @@ function formatUsd(value) {
91
91
  }
92
92
 
93
93
  // apps/cli/src/run.ts
94
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
94
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
95
95
  import { homedir as homedir3 } from "os";
96
- import { dirname as dirname3 } from "path";
97
96
 
98
97
  // packages/control/src/adminAuth.ts
99
98
  import { hash, verify } from "@node-rs/argon2";
@@ -153,10 +152,17 @@ function createAdminAuth(store, opts) {
153
152
  };
154
153
  }
155
154
  // packages/ir/src/capabilities.ts
155
+ var ANTHROPIC_NATIVE_TOOLS = {
156
+ anthropic: true,
157
+ openai: false,
158
+ kimi: false,
159
+ custom: false
160
+ };
156
161
  var PROVIDER_CAPABILITIES = {
157
162
  anthropic: { tools: true, images: true, reasoning: true },
158
163
  openai: { tools: true, images: true, reasoning: true },
159
- kimi: { tools: true, images: false, reasoning: false }
164
+ kimi: { tools: true, images: false, reasoning: false },
165
+ custom: { tools: true, images: true, reasoning: true }
160
166
  };
161
167
  // packages/ir/src/errors.ts
162
168
  var RETRYABLE = {
@@ -170,6 +176,7 @@ var RETRYABLE = {
170
176
  TIMEOUT: true,
171
177
  NETWORK: true,
172
178
  BAD_REQUEST: false,
179
+ CONFLICT: false,
173
180
  CONTENT_FILTER: false,
174
181
  NO_CANDIDATES: false,
175
182
  ALL_CANDIDATES_FAILED: false,
@@ -236,6 +243,82 @@ function usageFromPromptTotal(promptTokens, outputTokens, cacheReadTokens, cache
236
243
  cacheWriteTokens
237
244
  };
238
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
+ }
239
322
  // packages/control/src/config.ts
240
323
  var MIN_KEY_LENGTH = 16;
241
324
  var TRUTHY = new Set(["1", "true", "yes", "on"]);
@@ -310,7 +393,8 @@ var BODY_ORDER = {
310
393
  "parallel_tool_calls",
311
394
  "metadata"
312
395
  ],
313
- kimi: ["model", "messages", "tools", "tool_choice", "max_tokens", "temperature", "stream"]
396
+ kimi: ["model", "messages", "tools", "tool_choice", "max_tokens", "temperature", "stream"],
397
+ custom: []
314
398
  };
315
399
  function orderFields(obj, order) {
316
400
  const out = {};
@@ -411,10 +495,20 @@ var ANTHROPIC_MODELS = {
411
495
  id: "claude-haiku-4-5",
412
496
  label: "Claude Haiku 4.5",
413
497
  pricing: { input: 1, output: 5, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2 },
414
- limits: { contextWindow: 200000, maxOutputTokens: 64000 }
498
+ limits: { contextWindow: 200000, maxOutputTokens: 64000 },
499
+ reasoningForm: "budget"
415
500
  }
416
501
  ]
417
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
+ }
418
512
 
419
513
  // packages/providers/src/kimi/models.ts
420
514
  var KIMI_MODELS = {
@@ -486,7 +580,8 @@ var OPENAI_MODELS = {
486
580
  var PROVIDER_MODEL_CATALOG = {
487
581
  anthropic: ANTHROPIC_MODELS,
488
582
  openai: OPENAI_MODELS,
489
- kimi: KIMI_MODELS
583
+ kimi: KIMI_MODELS,
584
+ custom: { defaultModel: "", models: [] }
490
585
  };
491
586
  function catalogPricing(provider, model) {
492
587
  return PROVIDER_MODEL_CATALOG[provider]?.models.find((entry) => entry.id === model)?.pricing ?? null;
@@ -699,7 +794,8 @@ var kimi = {
699
794
  var PROFILES = {
700
795
  anthropic: { ...anthropic, order: envOrder("OMNI_ORDER_ANTHROPIC", anthropic.order) },
701
796
  openai: { ...openai, order: envOrder("OMNI_ORDER_OPENAI", openai.order) },
702
- kimi: { ...kimi, order: envOrder("OMNI_ORDER_KIMI", kimi.order) }
797
+ kimi: { ...kimi, order: envOrder("OMNI_ORDER_KIMI", kimi.order) },
798
+ custom: { headers: [], order: [] }
703
799
  };
704
800
 
705
801
  // packages/providers/src/sse.ts
@@ -759,14 +855,232 @@ function parseRecord(record) {
759
855
  `) };
760
856
  }
761
857
 
858
+ // packages/providers/src/anthropic/tools.ts
859
+ var COMMON = ["cache_control", "strict", "defer_loading", "allowed_callers"];
860
+ var COMMON_WITH_EXAMPLES = [...COMMON, "input_examples"];
861
+ var searchDomains = ["max_uses", "allowed_domains", "blocked_domains"];
862
+ var ANTHROPIC_TOOL_SPECS = {
863
+ web_search_20250305: {
864
+ family: "webSearch",
865
+ name: "web_search",
866
+ required: [],
867
+ optional: [...COMMON, ...searchDomains, "user_location"]
868
+ },
869
+ web_search_20260209: {
870
+ family: "webSearch",
871
+ name: "web_search",
872
+ required: [],
873
+ optional: [...COMMON, ...searchDomains, "user_location"]
874
+ },
875
+ web_search_20260318: {
876
+ family: "webSearch",
877
+ name: "web_search",
878
+ required: [],
879
+ optional: [...COMMON, ...searchDomains, "user_location", "response_inclusion"]
880
+ },
881
+ web_fetch_20250910: {
882
+ family: "webFetch",
883
+ name: "web_fetch",
884
+ required: [],
885
+ optional: [...COMMON, ...searchDomains, "citations", "max_content_tokens"]
886
+ },
887
+ web_fetch_20260209: {
888
+ family: "webFetch",
889
+ name: "web_fetch",
890
+ required: [],
891
+ optional: [...COMMON, ...searchDomains, "citations", "max_content_tokens"]
892
+ },
893
+ web_fetch_20260309: {
894
+ family: "webFetch",
895
+ name: "web_fetch",
896
+ required: [],
897
+ optional: [...COMMON, ...searchDomains, "citations", "max_content_tokens", "use_cache"]
898
+ },
899
+ web_fetch_20260318: {
900
+ family: "webFetch",
901
+ name: "web_fetch",
902
+ required: [],
903
+ optional: [
904
+ ...COMMON,
905
+ ...searchDomains,
906
+ "citations",
907
+ "max_content_tokens",
908
+ "use_cache",
909
+ "response_inclusion"
910
+ ]
911
+ },
912
+ code_execution_20250522: {
913
+ family: "codeExecution",
914
+ name: "code_execution",
915
+ required: [],
916
+ optional: [...COMMON]
917
+ },
918
+ code_execution_20250825: {
919
+ family: "codeExecution",
920
+ name: "code_execution",
921
+ required: [],
922
+ optional: [...COMMON]
923
+ },
924
+ code_execution_20260120: {
925
+ family: "codeExecution",
926
+ name: "code_execution",
927
+ required: [],
928
+ optional: [...COMMON]
929
+ },
930
+ code_execution_20260521: {
931
+ family: "codeExecution",
932
+ name: "code_execution",
933
+ required: [],
934
+ optional: [...COMMON]
935
+ },
936
+ bash_20241022: {
937
+ family: "bash",
938
+ name: "bash",
939
+ required: [],
940
+ optional: [...COMMON_WITH_EXAMPLES]
941
+ },
942
+ bash_20250124: {
943
+ family: "bash",
944
+ name: "bash",
945
+ required: [],
946
+ optional: [...COMMON_WITH_EXAMPLES]
947
+ },
948
+ text_editor_20241022: {
949
+ family: "textEditor",
950
+ name: "str_replace_editor",
951
+ required: [],
952
+ optional: [...COMMON_WITH_EXAMPLES]
953
+ },
954
+ text_editor_20250124: {
955
+ family: "textEditor",
956
+ name: "str_replace_editor",
957
+ required: [],
958
+ optional: [...COMMON_WITH_EXAMPLES]
959
+ },
960
+ text_editor_20250429: {
961
+ family: "textEditor",
962
+ name: "str_replace_based_edit_tool",
963
+ required: [],
964
+ optional: [...COMMON_WITH_EXAMPLES]
965
+ },
966
+ text_editor_20250728: {
967
+ family: "textEditor",
968
+ name: "str_replace_based_edit_tool",
969
+ required: [],
970
+ optional: [...COMMON_WITH_EXAMPLES, "max_characters"]
971
+ },
972
+ computer_20241022: {
973
+ family: "computer",
974
+ name: "computer",
975
+ required: ["display_width_px", "display_height_px"],
976
+ optional: [...COMMON_WITH_EXAMPLES, "display_number"]
977
+ },
978
+ computer_20250124: {
979
+ family: "computer",
980
+ name: "computer",
981
+ required: ["display_width_px", "display_height_px"],
982
+ optional: [...COMMON_WITH_EXAMPLES, "display_number"]
983
+ },
984
+ computer_20251124: {
985
+ family: "computer",
986
+ name: "computer",
987
+ required: ["display_width_px", "display_height_px"],
988
+ optional: [...COMMON_WITH_EXAMPLES, "display_number", "enable_zoom"]
989
+ },
990
+ memory_20250818: {
991
+ family: "memory",
992
+ name: "memory",
993
+ required: [],
994
+ optional: [...COMMON_WITH_EXAMPLES]
995
+ },
996
+ tool_search_tool_regex_20251119: {
997
+ family: "toolSearchRegex",
998
+ name: "tool_search_tool_regex",
999
+ required: [],
1000
+ optional: [...COMMON]
1001
+ },
1002
+ tool_search_tool_regex: {
1003
+ family: "toolSearchRegex",
1004
+ name: "tool_search_tool_regex",
1005
+ required: [],
1006
+ optional: [...COMMON]
1007
+ },
1008
+ tool_search_tool_bm25_20251119: {
1009
+ family: "toolSearchBm25",
1010
+ name: "tool_search_tool_bm25",
1011
+ required: [],
1012
+ optional: [...COMMON]
1013
+ },
1014
+ tool_search_tool_bm25: {
1015
+ family: "toolSearchBm25",
1016
+ name: "tool_search_tool_bm25",
1017
+ required: [],
1018
+ optional: [...COMMON]
1019
+ },
1020
+ advisor_20260301: {
1021
+ family: "advisor",
1022
+ name: "advisor",
1023
+ required: ["model"],
1024
+ optional: [...COMMON, "caching", "max_uses", "max_tokens"]
1025
+ },
1026
+ mcp_toolset: {
1027
+ family: "mcpToolset",
1028
+ required: ["mcp_server_name"],
1029
+ optional: ["cache_control", "configs", "default_config"]
1030
+ }
1031
+ };
1032
+ var ANTHROPIC_NATIVE_BLOCK_TYPES = new Set([
1033
+ "server_tool_use",
1034
+ "web_search_tool_result",
1035
+ "web_fetch_tool_result",
1036
+ "code_execution_tool_result",
1037
+ "bash_code_execution_tool_result",
1038
+ "text_editor_code_execution_tool_result",
1039
+ "mcp_tool_use",
1040
+ "mcp_tool_result",
1041
+ "tool_search_tool_result",
1042
+ "tool_reference",
1043
+ "advisor_tool_result",
1044
+ "advisor_result",
1045
+ "advisor_redacted_result",
1046
+ "container_upload",
1047
+ "compaction",
1048
+ "search_result",
1049
+ "document",
1050
+ "mid_conv_system",
1051
+ "tool_addition",
1052
+ "tool_removal",
1053
+ "fallback",
1054
+ "redacted_thinking"
1055
+ ]);
1056
+
762
1057
  // packages/providers/src/anthropic/decode.ts
763
1058
  var STOP_REASON = {
764
1059
  end_turn: "endTurn",
765
1060
  max_tokens: "maxTokens",
766
1061
  stop_sequence: "stopSequence",
767
1062
  tool_use: "toolUse",
768
- refusal: "contentFilter"
1063
+ refusal: "contentFilter",
1064
+ pause_turn: "pauseTurn"
769
1065
  };
1066
+ var KNOWN_EVENTS = new Set([
1067
+ "message_start",
1068
+ "content_block_start",
1069
+ "content_block_delta",
1070
+ "content_block_stop",
1071
+ "message_delta",
1072
+ "message_stop",
1073
+ "error",
1074
+ "ping"
1075
+ ]);
1076
+ var KNOWN_DELTAS = new Set([
1077
+ "text_delta",
1078
+ "thinking_delta",
1079
+ "signature_delta",
1080
+ "input_json_delta",
1081
+ "citations_delta",
1082
+ "compaction_delta"
1083
+ ]);
770
1084
  var ERROR_TYPE = {
771
1085
  overloaded_error: "OVERLOADED",
772
1086
  rate_limit_error: "RATE_LIMIT",
@@ -792,10 +1106,21 @@ async function* decodeAnthropic(messages) {
792
1106
  let outputTokens = 0;
793
1107
  let stopReason = "endTurn";
794
1108
  let terminal = false;
1109
+ const nativeBlocks = new Set;
1110
+ const protocolError = (message) => ({
1111
+ type: "error",
1112
+ code: "UPSTREAM",
1113
+ message,
1114
+ retryable: false
1115
+ });
795
1116
  for await (const msg of messages) {
796
1117
  const d = json(msg.data);
797
1118
  if (d === null)
798
1119
  continue;
1120
+ if (!KNOWN_EVENTS.has(msg.event)) {
1121
+ yield protocolError(`unrecognized Anthropic stream event "${msg.event}"`);
1122
+ return;
1123
+ }
799
1124
  switch (msg.event) {
800
1125
  case "message_start": {
801
1126
  const m = d.message ?? {};
@@ -820,6 +1145,18 @@ async function* decodeAnthropic(messages) {
820
1145
  index,
821
1146
  block: { type: "toolUse", id: String(cb.id), name: String(cb.name) }
822
1147
  };
1148
+ else if (cb.type !== undefined && ANTHROPIC_NATIVE_BLOCK_TYPES.has(cb.type)) {
1149
+ nativeBlocks.add(index);
1150
+ const { type: _blockType, ...data } = cb;
1151
+ yield {
1152
+ type: "blockStart",
1153
+ index,
1154
+ block: { type: "anthropicNative", blockType: cb.type, data }
1155
+ };
1156
+ } else {
1157
+ yield protocolError(`unrecognized Anthropic content block type "${String(cb.type)}"`);
1158
+ return;
1159
+ }
823
1160
  break;
824
1161
  }
825
1162
  case "content_block_delta": {
@@ -843,17 +1180,51 @@ async function* decodeAnthropic(messages) {
843
1180
  yield {
844
1181
  type: "blockDelta",
845
1182
  index,
846
- delta: { type: "toolJson", partial: delta.partial_json ?? "" }
1183
+ delta: nativeBlocks.has(index) ? { type: "anthropicNativeJson", partial: delta.partial_json ?? "" } : { type: "toolJson", partial: delta.partial_json ?? "" }
1184
+ };
1185
+ else if (delta.type === "citations_delta")
1186
+ yield {
1187
+ type: "blockDelta",
1188
+ index,
1189
+ delta: {
1190
+ type: "anthropicNative",
1191
+ deltaType: delta.type,
1192
+ data: { citation: delta.citation }
1193
+ }
847
1194
  };
1195
+ else if (delta.type === "compaction_delta")
1196
+ yield {
1197
+ type: "blockDelta",
1198
+ index,
1199
+ delta: {
1200
+ type: "anthropicNative",
1201
+ deltaType: delta.type,
1202
+ data: {
1203
+ ...delta.content === undefined ? {} : { content: delta.content },
1204
+ ...delta.encrypted_content === undefined ? {} : { encrypted_content: delta.encrypted_content }
1205
+ }
1206
+ }
1207
+ };
1208
+ else if (delta.type === undefined || !KNOWN_DELTAS.has(delta.type)) {
1209
+ yield protocolError(`unrecognized Anthropic content block delta "${String(delta.type)}"`);
1210
+ return;
1211
+ }
848
1212
  break;
849
1213
  }
850
1214
  case "content_block_stop":
1215
+ nativeBlocks.delete(d.index ?? 0);
851
1216
  yield { type: "blockEnd", index: d.index ?? 0 };
852
1217
  break;
853
1218
  case "message_delta": {
854
1219
  const reason = d.delta?.stop_reason;
855
- if (typeof reason === "string")
856
- stopReason = STOP_REASON[reason] ?? "endTurn";
1220
+ if (typeof reason === "string") {
1221
+ const mapped = STOP_REASON[reason];
1222
+ if (mapped === undefined) {
1223
+ yield protocolError(`unrecognized Anthropic stop reason "${reason}"`);
1224
+ return;
1225
+ }
1226
+ stopReason = mapped;
1227
+ }
857
1228
  inputTokens = d.usage?.input_tokens ?? inputTokens;
858
1229
  outputTokens = d.usage?.output_tokens ?? outputTokens;
859
1230
  break;
@@ -912,7 +1283,12 @@ function encodeBlock(b) {
912
1283
  const cache = wireCacheControl(cacheControlOf(b));
913
1284
  switch (b.type) {
914
1285
  case "text":
915
- return { type: "text", text: b.text, ...cache };
1286
+ return {
1287
+ type: "text",
1288
+ text: b.text,
1289
+ ...b.citations === undefined ? {} : { citations: b.citations },
1290
+ ...cache
1291
+ };
916
1292
  case "image":
917
1293
  return {
918
1294
  type: "image",
@@ -931,11 +1307,16 @@ function encodeBlock(b) {
931
1307
  is_error: b.isError,
932
1308
  ...cache
933
1309
  };
1310
+ case "anthropicNative":
1311
+ return { ...b.data, type: b.blockType, ...cache };
934
1312
  }
935
1313
  }
936
1314
  function encodeSystemTurn(content) {
937
- return content.flatMap((b) => b.type === "text" ? [b.text] : []).join(`
1315
+ if (content.every((block) => block.type === "text")) {
1316
+ return content.map((block) => block.text).join(`
938
1317
  `);
1318
+ }
1319
+ return content.map(encodeBlock);
939
1320
  }
940
1321
  function systemCacheControl(req) {
941
1322
  const cacheable = req.messages.flatMap((message) => message.content.flatMap((block) => block.type === "thinking" ? [] : [{ role: message.role, block }]));
@@ -947,6 +1328,23 @@ function systemCacheControl(req) {
947
1328
  lost: markedSystemBlocks.length > (promoted === undefined ? 0 : 1)
948
1329
  };
949
1330
  }
1331
+ function encodeTool(t) {
1332
+ if (t.provider === "anthropic") {
1333
+ return {
1334
+ type: t.type,
1335
+ ...t.name === "" ? {} : { name: t.name },
1336
+ ...t.wire,
1337
+ ...wireCacheControl(t.cacheControl)
1338
+ };
1339
+ }
1340
+ return {
1341
+ name: t.name,
1342
+ ...t.description === undefined ? {} : { description: t.description },
1343
+ input_schema: t.inputSchema,
1344
+ ...t.options ?? {},
1345
+ ...wireCacheControl(t.cacheControl)
1346
+ };
1347
+ }
950
1348
  function encodeToolChoice(c) {
951
1349
  switch (c.type) {
952
1350
  case "auto":
@@ -959,6 +1357,45 @@ function encodeToolChoice(c) {
959
1357
  return { type: "tool", name: c.name };
960
1358
  }
961
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
+ }
962
1399
  function toWire(req, model, opts) {
963
1400
  const degradations = [];
964
1401
  const note2 = (d) => {
@@ -1001,25 +1438,27 @@ function toWire(req, model, opts) {
1001
1438
  body.temperature = req.temperature;
1002
1439
  if (req.stopSequences !== undefined)
1003
1440
  body.stop_sequences = req.stopSequences;
1004
- if (req.tools !== undefined) {
1005
- body.tools = req.tools.map((t) => ({
1006
- name: t.name,
1007
- ...t.description === undefined ? {} : { description: t.description },
1008
- input_schema: t.inputSchema,
1009
- ...wireCacheControl(t.cacheControl)
1010
- }));
1011
- }
1441
+ if (req.tools !== undefined)
1442
+ body.tools = req.tools.map(encodeTool);
1012
1443
  if (req.toolChoice !== undefined)
1013
1444
  body.tool_choice = encodeToolChoice(req.toolChoice);
1014
1445
  if (req.reasoning !== undefined) {
1015
1446
  switch (req.reasoning.mode) {
1016
1447
  case "adaptive":
1448
+ if (anthropicReasoningForm(model) === "budget") {
1449
+ body.thinking = { type: "disabled" };
1450
+ note2("anthropic:adaptive-thinking-unsupported");
1451
+ break;
1452
+ }
1017
1453
  body.thinking = {
1018
1454
  type: "adaptive",
1019
1455
  ...req.reasoning.display === undefined ? {} : { display: req.reasoning.display }
1020
1456
  };
1021
1457
  if (req.reasoning.effort !== undefined) {
1022
- 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
+ };
1023
1462
  }
1024
1463
  break;
1025
1464
  case "budget":
@@ -1030,7 +1469,9 @@ function toWire(req, model, opts) {
1030
1469
  break;
1031
1470
  }
1032
1471
  }
1033
- 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);
1034
1475
  return { body, degradations };
1035
1476
  }
1036
1477
 
@@ -1094,115 +1535,6 @@ var anthropicAdapter = {
1094
1535
  return { events: decodeAnthropic(parseSse(res.body)), degradations: notes };
1095
1536
  }
1096
1537
  };
1097
- // packages/providers/src/http-client.ts
1098
- import { request as httpRequest } from "http";
1099
- import { request as httpsRequest } from "https";
1100
- import { Readable } from "stream";
1101
- function nodeHttpClient(options = {}) {
1102
- const logger2 = options.logger ?? noopLogger;
1103
- const now = options.now ?? (() => Date.now());
1104
- return (req) => new Promise((resolve, reject) => {
1105
- const url = new URL(req.url);
1106
- const startedAt = now();
1107
- let traced = false;
1108
- const trace = (status, failed = false) => {
1109
- if (traced || !logger2.enabled("debug"))
1110
- return;
1111
- traced = true;
1112
- logger2.debug("upstream http", {
1113
- provider: req.provider,
1114
- status,
1115
- host: url.host,
1116
- path: url.pathname,
1117
- durationMs: now() - startedAt,
1118
- reason: failed ? "transport error" : undefined
1119
- });
1120
- };
1121
- const send = url.protocol === "https:" ? httpsRequest : httpRequest;
1122
- const bodyBytes = Buffer.from(req.body, "utf8");
1123
- const headers = {};
1124
- for (const [name, value] of req.headers)
1125
- headers[name] = value;
1126
- if (req.body.length > 0 && !hasHeader(req, "content-length")) {
1127
- headers["Content-Length"] = bodyBytes.byteLength;
1128
- }
1129
- const outgoing = send({
1130
- protocol: url.protocol,
1131
- hostname: url.hostname,
1132
- port: url.port || (url.protocol === "https:" ? 443 : 80),
1133
- path: `${url.pathname}${url.search}`,
1134
- method: req.method,
1135
- headers,
1136
- setHost: !hasHeader(req, "host")
1137
- }, (incoming) => {
1138
- const chunks = [];
1139
- let buffered = null;
1140
- const responseHeaders = new Headers;
1141
- for (const [k, v] of Object.entries(incoming.headers)) {
1142
- if (Array.isArray(v))
1143
- for (const one of v)
1144
- responseHeaders.append(k, one);
1145
- else if (typeof v === "string")
1146
- responseHeaders.set(k, v);
1147
- }
1148
- trace(incoming.statusCode);
1149
- resolve({
1150
- status: incoming.statusCode ?? 0,
1151
- headers: responseHeaders,
1152
- body: Readable.toWeb(incoming),
1153
- text: () => {
1154
- buffered ??= new Promise((res, rej) => {
1155
- incoming.on("data", (c) => chunks.push(c));
1156
- incoming.on("end", () => res(Buffer.concat(chunks).toString("utf8")));
1157
- incoming.on("error", rej);
1158
- });
1159
- return buffered;
1160
- }
1161
- });
1162
- });
1163
- const onAbort = () => outgoing.destroy(new Error("aborted"));
1164
- outgoing.on("error", (err) => {
1165
- req.signal.removeEventListener("abort", onAbort);
1166
- trace(undefined, true);
1167
- reject(err);
1168
- });
1169
- outgoing.on("close", () => req.signal.removeEventListener("abort", onAbort));
1170
- if (req.signal.aborted) {
1171
- outgoing.destroy(new Error("aborted"));
1172
- return;
1173
- }
1174
- req.signal.addEventListener("abort", onAbort, { once: true });
1175
- if (bodyBytes.byteLength > 0)
1176
- outgoing.write(bodyBytes);
1177
- outgoing.end();
1178
- });
1179
- }
1180
- function hasHeader(req, lowerName) {
1181
- return req.headers.some(([name]) => name.toLowerCase() === lowerName);
1182
- }
1183
- // packages/providers/src/kimi-device.ts
1184
- import { randomUUID } from "crypto";
1185
- function mintKimiDevice() {
1186
- return {
1187
- deviceId: randomUUID(),
1188
- deviceName: "MacBook-Pro",
1189
- deviceModel: "MacBookPro18,3",
1190
- osVersion: "15.3.1"
1191
- };
1192
- }
1193
- function kimiDeviceHeaders(providerData) {
1194
- const deviceId = providerData.deviceId;
1195
- if (typeof deviceId !== "string" || deviceId.length === 0)
1196
- return [];
1197
- const str = (v) => typeof v === "string" && v.length > 0 ? v : "unknown";
1198
- return [
1199
- ["X-Msh-Device-Id", deviceId],
1200
- ["X-Msh-Device-Name", str(providerData.deviceName)],
1201
- ["X-Msh-Device-Model", str(providerData.deviceModel)],
1202
- ["X-Msh-Os-Version", str(providerData.osVersion)]
1203
- ];
1204
- }
1205
-
1206
1538
  // packages/providers/src/kimi/decode.ts
1207
1539
  var FINISH = {
1208
1540
  stop: "endTurn",
@@ -1313,7 +1645,7 @@ function encodeToolChoice2(c) {
1313
1645
  return { type: "function", function: { name: c.name } };
1314
1646
  }
1315
1647
  }
1316
- function toChatWire(req, model) {
1648
+ function toChatWire(req, model, vendor = "kimi") {
1317
1649
  const degradations = [];
1318
1650
  const note2 = (d) => {
1319
1651
  if (!degradations.includes(d))
@@ -1355,6 +1687,9 @@ function toChatWire(req, model) {
1355
1687
  content: block.content
1356
1688
  });
1357
1689
  break;
1690
+ case "anthropicNative":
1691
+ note2("kimi:anthropic-native-block-dropped");
1692
+ break;
1358
1693
  }
1359
1694
  }
1360
1695
  if (toolCalls.length > 0) {
@@ -1382,7 +1717,10 @@ function toChatWire(req, model) {
1382
1717
  if (req.stopSequences !== undefined)
1383
1718
  body.stop = req.stopSequences;
1384
1719
  if (req.tools !== undefined) {
1385
- body.tools = req.tools.map((t) => ({
1720
+ const custom = req.tools.filter((t) => t.provider === "custom");
1721
+ if (custom.length !== req.tools.length)
1722
+ note2("kimi:anthropic-tool-dropped");
1723
+ body.tools = custom.map((t) => ({
1386
1724
  type: "function",
1387
1725
  function: { name: t.name, description: t.description, parameters: t.inputSchema }
1388
1726
  }));
@@ -1391,44 +1729,10 @@ function toChatWire(req, model) {
1391
1729
  body.tool_choice = encodeToolChoice2(req.toolChoice);
1392
1730
  if (req.reasoning !== undefined)
1393
1731
  note2("kimi:reasoning-dropped");
1394
- Object.assign(body, req.vendor?.kimi ?? {});
1732
+ Object.assign(body, req.vendor?.[vendor] ?? {});
1395
1733
  return { body, degradations };
1396
1734
  }
1397
1735
 
1398
- // packages/providers/src/kimi/index.ts
1399
- var BASE_URL2 = "https://api.kimi.com/coding/v1/chat/completions";
1400
- var kimiAdapter = {
1401
- id: "kimi",
1402
- capabilities: PROVIDER_CAPABILITIES.kimi,
1403
- async send(req) {
1404
- const { body, degradations } = toChatWire(req.request, req.model);
1405
- const token = req.credentials.accessToken ?? req.credentials.apiKey;
1406
- if (token === null) {
1407
- throw new GatewayError("AUTH", "kimi credential has no token", { provider: "kimi" });
1408
- }
1409
- const protocol = [
1410
- ["Content-Type", "application/json"],
1411
- ["Accept", "text/event-stream"],
1412
- ["Authorization", `Bearer ${token}`],
1413
- ...kimiDeviceHeaders(req.credentials.providerData)
1414
- ];
1415
- const profile = PROFILES.kimi;
1416
- const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
1417
- const res = await req.http({
1418
- provider: "kimi",
1419
- url: BASE_URL2,
1420
- method: "POST",
1421
- headers,
1422
- body: JSON.stringify(orderFields({ ...body, stream: true }, BODY_ORDER.kimi)),
1423
- signal: req.signal
1424
- });
1425
- if (res.status < 200 || res.status >= 300)
1426
- throw await httpError(res, "kimi");
1427
- if (res.body === null)
1428
- throw new GatewayError("UPSTREAM", "empty response body", { provider: "kimi" });
1429
- return { events: decodeChat(parseSse(res.body)), degradations };
1430
- }
1431
- };
1432
1736
  // packages/providers/src/openai/decode.ts
1433
1737
  var ERROR_CODE = {
1434
1738
  rate_limit_exceeded: "RATE_LIMIT",
@@ -1649,6 +1953,9 @@ ${block.text}
1649
1953
  output: block.content
1650
1954
  });
1651
1955
  break;
1956
+ case "anthropicNative":
1957
+ note2("openai:anthropic-native-block-dropped");
1958
+ break;
1652
1959
  }
1653
1960
  }
1654
1961
  flush();
@@ -1672,7 +1979,10 @@ ${block.text}
1672
1979
  body.temperature = req.temperature;
1673
1980
  }
1674
1981
  if (req.tools !== undefined) {
1675
- body.tools = req.tools.map((t) => ({
1982
+ const custom = req.tools.filter((t) => t.provider === "custom");
1983
+ if (custom.length !== req.tools.length)
1984
+ note2("openai:anthropic-tool-dropped");
1985
+ body.tools = custom.map((t) => ({
1676
1986
  type: "function",
1677
1987
  name: t.name,
1678
1988
  description: t.description,
@@ -1693,11 +2003,195 @@ ${block.text}
1693
2003
  if (req.reasoning.mode === "budget") {
1694
2004
  degradations.push("openai:reasoning-budget-dropped");
1695
2005
  }
1696
- }
1697
- Object.assign(body, req.vendor?.openai ?? {});
1698
- return { body, degradations };
2006
+ }
2007
+ Object.assign(body, req.vendor?.openai ?? {});
2008
+ return { body, degradations };
2009
+ }
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
+ ];
1699
2159
  }
1700
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
+ };
1701
2195
  // packages/providers/src/openai/index.ts
1702
2196
  var OAUTH_URL = "https://chatgpt.com/backend-api/codex/responses";
1703
2197
  var API_URL = "https://api.openai.com/v1/responses";
@@ -1910,11 +2404,11 @@ function pendingError(code) {
1910
2404
  error[PENDING_MARKER] = true;
1911
2405
  return error;
1912
2406
  }
1913
- function isRecord(value) {
2407
+ function isRecord2(value) {
1914
2408
  return typeof value === "object" && value !== null;
1915
2409
  }
1916
2410
  function recordFrom(value) {
1917
- return isRecord(value) ? value : null;
2411
+ return isRecord2(value) ? value : null;
1918
2412
  }
1919
2413
  function stringFrom(value, field) {
1920
2414
  const candidate = value[field];
@@ -2117,7 +2611,7 @@ function createPendingFlows(opts) {
2117
2611
  }
2118
2612
 
2119
2613
  // packages/control/src/connect.ts
2120
- var PROVIDER_IDS = ["anthropic", "openai", "kimi"];
2614
+ var PROVIDER_IDS = ["anthropic", "openai", "kimi", "custom"];
2121
2615
  var FLOW_TTL_MS = 600000;
2122
2616
  function isProviderId(value) {
2123
2617
  return typeof value === "string" && PROVIDER_IDS.includes(value);
@@ -2158,6 +2652,8 @@ function createConnectFlows(deps) {
2158
2652
  }
2159
2653
  async function complete(flow, code) {
2160
2654
  const provider = deps.providers[flow.provider];
2655
+ if (provider === undefined)
2656
+ throw new GatewayError("BAD_REQUEST", "provider does not support OAuth");
2161
2657
  const result = await provider.exchange({ code, pending: flow.pending }, { http: deps.http, now: deps.now });
2162
2658
  const id = crypto.randomUUID();
2163
2659
  await deps.store.credentials.create({
@@ -2201,6 +2697,9 @@ function createConnectFlows(deps) {
2201
2697
  }
2202
2698
  const label = typeof labelInput === "string" && labelInput.trim().length > 0 ? labelInput.trim() : providerInput;
2203
2699
  const provider = deps.providers[providerInput];
2700
+ if (provider === undefined) {
2701
+ throw new GatewayError("BAD_REQUEST", "provider does not support OAuth");
2702
+ }
2204
2703
  const redirectUri = callbackUri(providerInput);
2205
2704
  const start = provider.begin === undefined ? provider.start({ redirectUri }) : await (async () => {
2206
2705
  const initial = provider.start({ redirectUri });
@@ -13453,12 +13952,12 @@ function describe(description) {
13453
13952
  ch._zod.check = () => {};
13454
13953
  return ch;
13455
13954
  }
13456
- function meta(metadata) {
13955
+ function meta(metadata2) {
13457
13956
  const ch = new $ZodCheck({ check: "meta" });
13458
13957
  ch._zod.onattach = [
13459
13958
  (inst) => {
13460
13959
  const existing = globalRegistry.get(inst) ?? {};
13461
- globalRegistry.add(inst, { ...existing, ...metadata });
13960
+ globalRegistry.add(inst, { ...existing, ...metadata2 });
13462
13961
  }
13463
13962
  ];
13464
13963
  ch._zod.check = () => {};
@@ -16590,20 +17089,36 @@ function parseOrThrow(schema, body2) {
16590
17089
  const path = issue2?.path.join(".") ?? "(root)";
16591
17090
  throw new GatewayError("BAD_REQUEST", `${path}: ${issue2?.message ?? "invalid request"}`);
16592
17091
  }
16593
- var providerIdSchema = exports_external.enum(["anthropic", "openai", "kimi"]);
17092
+ var providerIdSchema = exports_external.enum(["anthropic", "openai", "kimi", "custom"]);
16594
17093
  var dryRunSchema = exports_external.object({
16595
17094
  tools: exports_external.boolean().default(false),
16596
17095
  images: exports_external.boolean().default(false),
16597
17096
  reasoning: exports_external.boolean().default(false)
16598
17097
  }).strict();
16599
- var modelSchema = exports_external.object({
16600
- id: exports_external.string().min(1).refine((value) => !value.toLowerCase().startsWith("claude/"), {
16601
- message: 'model id must not start with "claude/": that prefix is reserved for discovery mirrors'
16602
- }),
16603
- strategy: exports_external.enum(["score", "priority", "roundRobin", "weighted"]),
16604
- isAlias: exports_external.boolean(),
16605
- targets: exports_external.array(exports_external.object({
16606
- provider: providerIdSchema,
17098
+ var targetSchema = exports_external.discriminatedUnion("provider", [
17099
+ exports_external.object({
17100
+ provider: exports_external.enum(["anthropic", "openai", "kimi"]),
17101
+ model: exports_external.string().min(1),
17102
+ tier: exports_external.number().int().min(1),
17103
+ weight: exports_external.number().positive(),
17104
+ costPerMTok: exports_external.object({
17105
+ input: exports_external.number().min(0),
17106
+ output: exports_external.number().min(0),
17107
+ cacheRead: exports_external.number().min(0).optional(),
17108
+ cacheWrite5m: exports_external.number().min(0).optional(),
17109
+ cacheWrite1h: exports_external.number().min(0).optional()
17110
+ }),
17111
+ contextWindow: exports_external.number().int().positive().optional(),
17112
+ maxOutputTokens: exports_external.number().int().positive().optional(),
17113
+ capabilities: exports_external.object({
17114
+ tools: exports_external.boolean(),
17115
+ images: exports_external.boolean(),
17116
+ reasoning: exports_external.boolean()
17117
+ })
17118
+ }).strict(),
17119
+ exports_external.object({
17120
+ provider: exports_external.literal("custom"),
17121
+ endpointId: exports_external.string().trim().min(1),
16607
17122
  model: exports_external.string().min(1),
16608
17123
  tier: exports_external.number().int().min(1),
16609
17124
  weight: exports_external.number().positive(),
@@ -16621,7 +17136,15 @@ var modelSchema = exports_external.object({
16621
17136
  images: exports_external.boolean(),
16622
17137
  reasoning: exports_external.boolean()
16623
17138
  })
16624
- })).min(1, "a virtual model needs at least one target")
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")
16625
17148
  });
16626
17149
  var keyCreateSchema = exports_external.object({
16627
17150
  label: exports_external.string().min(1).default("api key"),
@@ -16635,14 +17158,15 @@ var settingsSchema = exports_external.object({
16635
17158
  quota: exports_external.number(),
16636
17159
  cost: exports_external.number(),
16637
17160
  latency: exports_external.number(),
16638
- recency: exports_external.number()
17161
+ load: exports_external.number()
16639
17162
  }).strict(),
16640
17163
  maxAttempts: exports_external.number().int().min(1).max(10),
16641
- requestDeadlineMs: exports_external.number().int().positive(),
17164
+ requestDeadlineMs: exports_external.number().int().min(0),
16642
17165
  breakerThreshold: exports_external.number().int().min(1),
16643
17166
  breakerCooldownMs: exports_external.number().int().positive(),
16644
17167
  logRetentionDays: exports_external.number().int().min(1),
16645
- quotaPollIntervalMs: exports_external.number().int().min(0)
17168
+ quotaPollIntervalMs: exports_external.number().int().min(0),
17169
+ rtkEnabled: exports_external.boolean()
16646
17170
  });
16647
17171
  var credentialPatchSchema = exports_external.object({
16648
17172
  label: exports_external.string().min(1).optional(),
@@ -16699,14 +17223,48 @@ async function getCredential(store, id) {
16699
17223
  throw new GatewayError("BAD_REQUEST", "no such credential");
16700
17224
  return summarizeCredential(credential);
16701
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
+ }
16702
17253
  async function createApiKeyCredential(store, input, logger2 = noopLogger) {
16703
17254
  const provider = parseOrThrow(providerIdSchema, input.provider);
16704
- if (typeof input.apiKey !== "string" || input.apiKey.trim().length === 0) {
16705
- throw new GatewayError("BAD_REQUEST", "apiKey: must not be empty");
16706
- }
17255
+ const apiKey = requiredString(input.apiKey, "apiKey");
16707
17256
  if (input.label !== undefined && typeof input.label !== "string") {
16708
17257
  throw new GatewayError("BAD_REQUEST", "label: must be a string");
16709
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
+ }
16710
17268
  const label = input.label?.trim() || `${provider} api key`;
16711
17269
  const created = await store.credentials.create({
16712
17270
  id: crypto.randomUUID(),
@@ -16718,12 +17276,12 @@ async function createApiKeyCredential(store, input, logger2 = noopLogger) {
16718
17276
  weight: 1,
16719
17277
  expiresAt: null,
16720
17278
  accountEmail: null,
16721
- providerData: {},
17279
+ providerData,
16722
17280
  disabledReason: null,
16723
17281
  disabledAt: null,
16724
17282
  accessToken: null,
16725
17283
  refreshToken: null,
16726
- apiKey: input.apiKey,
17284
+ apiKey,
16727
17285
  idToken: null
16728
17286
  });
16729
17287
  logger2.info("credential added", { credentialId: created.id, provider: created.provider });
@@ -16829,6 +17387,11 @@ async function buildSnapshot(store, now) {
16829
17387
  }
16830
17388
 
16831
17389
  // packages/router/src/filters.ts
17390
+ function needsAnthropicNative(request2) {
17391
+ if (request2.tools?.some((t) => t.provider === "anthropic") === true)
17392
+ return true;
17393
+ return request2.messages.some((m) => m.content.some((b) => b.type === "anthropicNative"));
17394
+ }
16832
17395
  function requiredCapabilities(request2) {
16833
17396
  const images = request2.messages.some((m) => m.content.some((b) => b.type === "image"));
16834
17397
  return {
@@ -16845,13 +17408,17 @@ function eligible(input) {
16845
17408
  const { request: request2, model, snapshot, now } = input;
16846
17409
  const { breakerThreshold, breakerCooldownMs } = snapshot.settings;
16847
17410
  const need = requiredCapabilities(request2);
17411
+ const needNative = needsAnthropicNative(request2);
16848
17412
  const pairs = [];
16849
17413
  const excluded = [];
16850
17414
  for (const target of model.targets) {
16851
- const missing = ["tools", "images", "reasoning"].find((cap) => need[cap] && !target.capabilities[cap]);
17415
+ const missing = needNative && !ANTHROPIC_NATIVE_TOOLS[target.provider] ? "anthropicTools" : ["tools", "images", "reasoning"].find((cap) => need[cap] && !target.capabilities[cap]);
16852
17416
  for (const credential of snapshot.credentials) {
16853
17417
  if (credential.provider !== target.provider)
16854
17418
  continue;
17419
+ if (target.provider === "custom" && credential.providerData.endpointId !== target.endpointId) {
17420
+ continue;
17421
+ }
16855
17422
  const drop = (reason) => {
16856
17423
  excluded.push({ credentialId: credential.id, model: target.model, reason });
16857
17424
  };
@@ -16923,6 +17490,22 @@ function quotaHeadroom(credential, windows, now, pollIntervalMs) {
16923
17490
  return Math.min(...usable.map((w) => paceAdjusted(w, now)));
16924
17491
  }
16925
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
+
16926
17509
  // packages/router/src/score.ts
16927
17510
  var UNKNOWN = 0.5;
16928
17511
  function lowerIsBetter(value, min, max) {
@@ -16930,43 +17513,49 @@ function lowerIsBetter(value, min, max) {
16930
17513
  return 1;
16931
17514
  return (max - value) / (max - min);
16932
17515
  }
16933
- function blendedCost(input, output) {
16934
- 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;
16935
17533
  }
16936
17534
  function score(pairs, input) {
16937
- const { snapshot, now } = input;
17535
+ const { snapshot, now, load } = input;
16938
17536
  const w = snapshot.settings.weights;
16939
17537
  const tiers = pairs.map((p) => p.target.tier);
16940
17538
  const minTier = Math.min(...tiers);
16941
17539
  const maxTier = Math.max(...tiers);
16942
- const costs = pairs.map((p) => blendedCost(p.target.costPerMTok.input, p.target.costPerMTok.output));
16943
- const minCost = Math.min(...costs);
16944
- const maxCost = Math.max(...costs);
16945
- 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) => {
16946
17543
  const h = snapshot.health.get(healthKey(p.credential.id, p.target.model));
16947
17544
  return h?.ewmaTtftMs != null ? [h.ewmaTtftMs] : [];
16948
- });
16949
- const minLatency = latencies.length > 0 ? Math.min(...latencies) : 0;
16950
- const maxLatency = latencies.length > 0 ? Math.max(...latencies) : 0;
16951
- const idleTimes = pairs.map((p) => {
16952
- const h = snapshot.health.get(healthKey(p.credential.id, p.target.model));
16953
- return h?.lastUsedAt == null ? Number.POSITIVE_INFINITY : now - h.lastUsedAt;
16954
- });
16955
- const finiteIdle = idleTimes.filter(Number.isFinite);
16956
- const maxIdle = finiteIdle.length > 0 ? Math.max(...finiteIdle) : 1;
17545
+ }));
16957
17546
  return pairs.map((pair, i) => {
16958
- 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);
16959
17549
  const tier = lowerIsBetter(pair.target.tier, minTier, maxTier);
16960
- let health = 1 / (1 + (h?.consecutiveFailures ?? 0));
16961
- if (h?.breakerState === "open" || h?.breakerState === "halfOpen")
16962
- health *= 0.5;
17550
+ const inflight = load.get(key) ?? 0;
17551
+ const loadTerm = 1 / (1 + inflight);
17552
+ const health = healthScore(h);
16963
17553
  const quota = quotaHeadroom(pair.credential, snapshot.quota.get(pair.credential.id) ?? [], now, snapshot.settings.quotaPollIntervalMs);
16964
- const cost = maxCost === 0 ? UNKNOWN : lowerIsBetter(costs[i], minCost, maxCost);
16965
- const latency = h?.ewmaTtftMs == null ? UNKNOWN : lowerIsBetter(h.ewmaTtftMs, minLatency, maxLatency);
16966
- const idle = idleTimes[i];
16967
- const recency = Number.isFinite(idle) ? Math.min(1, idle / (maxIdle || 1)) : 1;
16968
- const reasons = { tier, health, quota, cost, latency, recency };
16969
- 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;
16970
17559
  return {
16971
17560
  credential: pair.credential,
16972
17561
  target: pair.target,
@@ -16979,8 +17568,8 @@ function score(pairs, input) {
16979
17568
  var PROVIDERS = new Set(Object.keys(PROVIDER_CAPABILITIES));
16980
17569
 
16981
17570
  // packages/router/src/index.ts
16982
- function weightedShuffle(candidates, rand, headroom) {
16983
- 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);
16984
17573
  const total = candidates.reduce((sum, c) => sum + drawWeight(c), 0);
16985
17574
  if (total <= 0)
16986
17575
  return [...candidates].sort((a, b) => b.score - a.score);
@@ -17003,6 +17592,8 @@ function rank(input) {
17003
17592
  return { candidates: [], excluded };
17004
17593
  const scored = score(pairs, input);
17005
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;
17006
17597
  switch (input.model.strategy) {
17007
17598
  case "priority":
17008
17599
  scored.sort((a, b) => a.target.tier - b.target.tier || b.score - a.score);
@@ -17013,11 +17604,11 @@ function rank(input) {
17013
17604
  return h?.lastUsedAt == null ? Number.POSITIVE_INFINITY : input.now - h.lastUsedAt;
17014
17605
  };
17015
17606
  const spent = (c) => headroom(c) < QUOTA_FLOOR ? 1 : 0;
17016
- 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));
17017
17608
  break;
17018
17609
  }
17019
17610
  case "weighted":
17020
- return { candidates: weightedShuffle(scored, input.rand, headroom), excluded };
17611
+ return { candidates: weightedShuffle(scored, input.rand, headroom, health), excluded };
17021
17612
  case "score":
17022
17613
  scored.sort((a, b) => b.score - a.score);
17023
17614
  break;
@@ -17043,10 +17634,14 @@ async function dryRun(deps, modelId, input) {
17043
17634
  }
17044
17635
  ],
17045
17636
  stream: false,
17046
- ...need.tools ? { tools: [{ name: "probe", description: "", inputSchema: { type: "object" } }] } : {},
17637
+ ...need.tools ? {
17638
+ tools: [
17639
+ { provider: "custom", name: "probe", description: "", inputSchema: { type: "object" } }
17640
+ ]
17641
+ } : {},
17047
17642
  ...need.reasoning ? { reasoning: { mode: "adaptive" } } : {}
17048
17643
  };
17049
- const result = rank({ request: probe, model, snapshot, now, rand: 0 });
17644
+ const result = rank({ request: probe, model, snapshot, now, rand: 0, load: new Map });
17050
17645
  return {
17051
17646
  modelId: model.id,
17052
17647
  strategy: model.strategy,
@@ -17103,20 +17698,24 @@ function unhex(s) {
17103
17698
  out[i] = Number.parseInt(s.slice(i * 2, i * 2 + 2), 16);
17104
17699
  return out;
17105
17700
  }
17106
- // packages/store/src/types.ts
17107
- var DEFAULT_SETTINGS = {
17108
- weights: { tier: 10, health: 3, quota: 2, cost: 1, latency: 1, recency: 0.5 },
17109
- maxAttempts: 3,
17110
- requestDeadlineMs: 120000,
17111
- breakerThreshold: 3,
17112
- breakerCooldownMs: 30000,
17113
- logRetentionDays: 30,
17114
- quotaPollIntervalMs: 300000
17115
- };
17116
-
17117
17701
  // packages/store/src/sqlite/config.ts
17118
17702
  var SETTINGS_KEY = "settings";
17119
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
+ }
17120
17719
  function createConfigRepo(db, emit2 = () => {}) {
17121
17720
  const readRaw = (key) => db.query("SELECT value FROM settings WHERE key = ?").get(key)?.value ?? null;
17122
17721
  const writeRaw = (key, value) => {
@@ -17147,12 +17746,18 @@ function createConfigRepo(db, emit2 = () => {}) {
17147
17746
  const raw = readRaw(SETTINGS_KEY);
17148
17747
  if (raw === null)
17149
17748
  return DEFAULT_SETTINGS;
17150
- const stored = JSON.parse(raw);
17151
- return {
17152
- ...DEFAULT_SETTINGS,
17153
- ...stored,
17154
- weights: { ...DEFAULT_SETTINGS.weights, ...stored.weights }
17155
- };
17749
+ try {
17750
+ const parsed = JSON.parse(raw);
17751
+ const stored = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
17752
+ return {
17753
+ ...DEFAULT_SETTINGS,
17754
+ ...stored,
17755
+ rtkEnabled: stored.rtkEnabled === true,
17756
+ weights: knownWeights(stored.weights)
17757
+ };
17758
+ } catch {
17759
+ return DEFAULT_SETTINGS;
17760
+ }
17156
17761
  },
17157
17762
  async putSettings(patch) {
17158
17763
  const current = await this.getSettings();
@@ -17594,6 +18199,20 @@ ALTER TABLE request_logs ADD COLUMN state TEXT NOT NULL DEFAULT 'done';
17594
18199
  CREATE INDEX idx_request_logs_pending ON request_logs(state) WHERE state = 'pending';
17595
18200
  `;
17596
18201
 
18202
+ // packages/store/src/sqlite/migrations/005_rtk_metrics.sql
18203
+ var _005_rtk_metrics_default = `ALTER TABLE request_logs ADD COLUMN rtk_applied INTEGER NOT NULL DEFAULT 0;
18204
+ ALTER TABLE request_logs ADD COLUMN rtk_filter_hits INTEGER NOT NULL DEFAULT 0;
18205
+ ALTER TABLE request_logs ADD COLUMN rtk_original_code_units INTEGER NOT NULL DEFAULT 0;
18206
+ ALTER TABLE request_logs ADD COLUMN rtk_compressed_code_units INTEGER NOT NULL DEFAULT 0;
18207
+ ALTER TABLE request_logs ADD COLUMN rtk_estimated_tokens_saved INTEGER NOT NULL DEFAULT 0;
18208
+ ALTER TABLE request_logs ADD COLUMN rtk_filters TEXT NOT NULL DEFAULT '[]';
18209
+ `;
18210
+
18211
+ // packages/store/src/sqlite/migrations/006_rtk_usage.sql
18212
+ var _006_rtk_usage_default = `ALTER TABLE usage_daily ADD COLUMN rtk_saved_tokens INTEGER NOT NULL DEFAULT 0;
18213
+ ALTER TABLE usage_daily ADD COLUMN rtk_applied_requests INTEGER NOT NULL DEFAULT 0;
18214
+ `;
18215
+
17597
18216
  // packages/store/src/sqlite/rollup.ts
17598
18217
  function startOfLocalDay(at) {
17599
18218
  const day = new Date(at);
@@ -17604,8 +18223,8 @@ var UPSERT = `
17604
18223
  INSERT INTO usage_daily
17605
18224
  (day, provider, credential_id, requested_model, resolved_model, api_key_id,
17606
18225
  requests, errors, input_tokens, output_tokens, cache_read_tokens,
17607
- cache_write_tokens, cost_usd, duration_ms_sum)
17608
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
18226
+ cache_write_tokens, rtk_saved_tokens, rtk_applied_requests, cost_usd, duration_ms_sum)
18227
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
17609
18228
  ON CONFLICT (day, provider, credential_id, requested_model, resolved_model, api_key_id)
17610
18229
  DO UPDATE SET
17611
18230
  requests = requests + excluded.requests,
@@ -17613,8 +18232,10 @@ var UPSERT = `
17613
18232
  input_tokens = input_tokens + excluded.input_tokens,
17614
18233
  output_tokens = output_tokens + excluded.output_tokens,
17615
18234
  cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
17616
- cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
17617
- cost_usd = cost_usd + excluded.cost_usd,
18235
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
18236
+ rtk_saved_tokens = rtk_saved_tokens + excluded.rtk_saved_tokens,
18237
+ rtk_applied_requests = rtk_applied_requests + excluded.rtk_applied_requests,
18238
+ cost_usd = cost_usd + excluded.cost_usd,
17618
18239
  duration_ms_sum = duration_ms_sum + excluded.duration_ms_sum`;
17619
18240
  function keyOf(log) {
17620
18241
  return [
@@ -17634,6 +18255,8 @@ function countersOf(log) {
17634
18255
  outputTokens: log.outputTokens,
17635
18256
  cacheReadTokens: log.cacheReadTokens,
17636
18257
  cacheWriteTokens: log.cacheWriteTokens,
18258
+ rtkSavedTokens: log.rtkEstimatedTokensSaved,
18259
+ rtkAppliedRequests: log.rtkApplied ? 1 : 0,
17637
18260
  costUsd: log.costUsd,
17638
18261
  durationMsSum: log.durationMs
17639
18262
  };
@@ -17647,6 +18270,8 @@ function upsert(db, key, c) {
17647
18270
  c.outputTokens,
17648
18271
  c.cacheReadTokens,
17649
18272
  c.cacheWriteTokens,
18273
+ c.rtkSavedTokens,
18274
+ c.rtkAppliedRequests,
17650
18275
  c.costUsd,
17651
18276
  c.durationMsSum
17652
18277
  ]);
@@ -17677,6 +18302,8 @@ function backfillDaily(db) {
17677
18302
  outputTokens: 0,
17678
18303
  cacheReadTokens: 0,
17679
18304
  cacheWriteTokens: 0,
18305
+ rtkSavedTokens: 0,
18306
+ rtkAppliedRequests: 0,
17680
18307
  costUsd: 0,
17681
18308
  durationMsSum: 0
17682
18309
  };
@@ -17695,13 +18322,42 @@ function backfillDaily(db) {
17695
18322
  upsert(db, group.key, group.counters);
17696
18323
  return groups.size;
17697
18324
  }
18325
+ function backfillRtkUsage(db) {
18326
+ const groups = new Map;
18327
+ for (const row of db.query(`SELECT at, api_key_id, requested_model, resolved_provider, resolved_model, credential_id,
18328
+ rtk_applied, rtk_estimated_tokens_saved
18329
+ FROM request_logs
18330
+ WHERE state = 'done'`).all()) {
18331
+ const key = [
18332
+ startOfLocalDay(row.at),
18333
+ row.resolved_provider ?? "",
18334
+ row.credential_id ?? "",
18335
+ row.requested_model,
18336
+ row.resolved_model ?? "",
18337
+ row.api_key_id ?? ""
18338
+ ];
18339
+ const id = key.join("\x00");
18340
+ const group = groups.get(id) ?? { key, saved: 0, applied: 0 };
18341
+ group.saved += row.rtk_estimated_tokens_saved;
18342
+ group.applied += row.rtk_applied === 1 ? 1 : 0;
18343
+ groups.set(id, group);
18344
+ }
18345
+ for (const { key, saved, applied } of groups.values()) {
18346
+ db.run(`UPDATE usage_daily
18347
+ SET rtk_saved_tokens = ?, rtk_applied_requests = ?
18348
+ WHERE day = ? AND provider = ? AND credential_id = ? AND requested_model = ?
18349
+ AND resolved_model = ? AND api_key_id = ?`, [saved, applied, ...key]);
18350
+ }
18351
+ }
17698
18352
 
17699
18353
  // packages/store/src/sqlite/db.ts
17700
18354
  var MIGRATIONS = [
17701
18355
  { id: 1, sql: _001_init_default },
17702
18356
  { id: 2, sql: _002_usage_daily_default, after: backfillDaily },
17703
18357
  { id: 3, sql: _003_quota_snapshot_default },
17704
- { id: 4, sql: _004_request_state_default }
18358
+ { id: 4, sql: _004_request_state_default },
18359
+ { id: 5, sql: _005_rtk_metrics_default },
18360
+ { id: 6, sql: _006_rtk_usage_default, after: backfillRtkUsage }
17705
18361
  ];
17706
18362
  function openDb(path) {
17707
18363
  const db = new Database(path, { create: true });
@@ -17770,7 +18426,38 @@ function createKeyRepo(db) {
17770
18426
  }
17771
18427
  };
17772
18428
  }
18429
+ // packages/rtk/src/catalog.ts
18430
+ var RTK_FILTER_IDS = [
18431
+ "git-diff",
18432
+ "git-status",
18433
+ "git-log",
18434
+ "grep",
18435
+ "path-list",
18436
+ "numbered-read",
18437
+ "build-output",
18438
+ "test-output",
18439
+ "deduplicate-log",
18440
+ "smart-truncate",
18441
+ "lint-output",
18442
+ "package-output",
18443
+ "tree-output",
18444
+ "git-operation",
18445
+ "docker-build"
18446
+ ];
18447
+ var FILTER_IDS = new Set(RTK_FILTER_IDS);
18448
+ function isRtkFilterId(value) {
18449
+ return FILTER_IDS.has(value);
18450
+ }
18451
+
17773
18452
  // packages/store/src/sqlite/usage.ts
18453
+ function parseRtkFilters(raw) {
18454
+ try {
18455
+ const parsed = JSON.parse(raw);
18456
+ return Array.isArray(parsed) ? parsed.filter(isRtkFilterId) : [];
18457
+ } catch {
18458
+ return [];
18459
+ }
18460
+ }
17774
18461
  var toLog = (r) => ({
17775
18462
  id: r.id,
17776
18463
  state: r.state === "pending" ? "pending" : "done",
@@ -17790,7 +18477,13 @@ var toLog = (r) => ({
17790
18477
  ttftMs: r.ttft_ms,
17791
18478
  durationMs: r.duration_ms,
17792
18479
  costUsd: r.cost_usd,
17793
- degradations: JSON.parse(r.degradations)
18480
+ degradations: JSON.parse(r.degradations),
18481
+ rtkApplied: r.rtk_applied === 1,
18482
+ rtkFilterHits: r.rtk_filter_hits,
18483
+ rtkOriginalCodeUnits: r.rtk_original_code_units,
18484
+ rtkCompressedCodeUnits: r.rtk_compressed_code_units,
18485
+ rtkEstimatedTokensSaved: r.rtk_estimated_tokens_saved,
18486
+ rtkFilters: parseRtkFilters(r.rtk_filters)
17794
18487
  });
17795
18488
  var GROUP_COLUMN = {
17796
18489
  raw: {
@@ -17835,8 +18528,9 @@ function label(value) {
17835
18528
  var COLUMNS = `(id, state, at, api_key_id, requested_model, resolved_provider, resolved_model,
17836
18529
  credential_id, attempts, status, error_code, input_tokens, output_tokens,
17837
18530
  cache_read_tokens, cache_write_tokens, ttft_ms, duration_ms, cost_usd,
17838
- degradations)`;
17839
- var PLACEHOLDERS = "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
18531
+ degradations, rtk_applied, rtk_filter_hits, rtk_original_code_units,
18532
+ rtk_compressed_code_units, rtk_estimated_tokens_saved, rtk_filters)`;
18533
+ var PLACEHOLDERS = "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
17840
18534
  function values(log, state) {
17841
18535
  return [
17842
18536
  log.id,
@@ -17857,7 +18551,13 @@ function values(log, state) {
17857
18551
  log.ttftMs,
17858
18552
  log.durationMs,
17859
18553
  log.costUsd,
17860
- JSON.stringify(log.degradations)
18554
+ JSON.stringify(log.degradations),
18555
+ log.rtkApplied === true ? 1 : 0,
18556
+ log.rtkFilterHits ?? 0,
18557
+ log.rtkOriginalCodeUnits ?? 0,
18558
+ log.rtkCompressedCodeUnits ?? 0,
18559
+ log.rtkEstimatedTokensSaved ?? 0,
18560
+ JSON.stringify(log.rtkFilters ?? [])
17861
18561
  ];
17862
18562
  }
17863
18563
  var COMPLETE = `INSERT INTO request_logs ${COLUMNS} VALUES ${PLACEHOLDERS}
@@ -17878,7 +18578,13 @@ var COMPLETE = `INSERT INTO request_logs ${COLUMNS} VALUES ${PLACEHOLDERS}
17878
18578
  ttft_ms = excluded.ttft_ms,
17879
18579
  duration_ms = excluded.duration_ms,
17880
18580
  cost_usd = excluded.cost_usd,
17881
- degradations = excluded.degradations`;
18581
+ degradations = excluded.degradations,
18582
+ rtk_applied = excluded.rtk_applied,
18583
+ rtk_filter_hits = excluded.rtk_filter_hits,
18584
+ rtk_original_code_units = excluded.rtk_original_code_units,
18585
+ rtk_compressed_code_units = excluded.rtk_compressed_code_units,
18586
+ rtk_estimated_tokens_saved = excluded.rtk_estimated_tokens_saved,
18587
+ rtk_filters = excluded.rtk_filters`;
17882
18588
  function createUsageRepo(db) {
17883
18589
  const complete = db.transaction((log) => {
17884
18590
  db.run(COMPLETE, values(log, "done"));
@@ -17923,6 +18629,8 @@ function createUsageRepo(db) {
17923
18629
  COALESCE(SUM(output_tokens), 0) AS output_tokens,
17924
18630
  COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
17925
18631
  COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
18632
+ COALESCE(SUM(${daily ? "rtk_saved_tokens" : "rtk_estimated_tokens_saved"}), 0) AS rtk_saved_tokens,
18633
+ COALESCE(SUM(${daily ? "rtk_applied_requests" : "CASE WHEN rtk_applied = 1 THEN 1 ELSE 0 END"}), 0) AS rtk_applied_requests,
17926
18634
  COALESCE(SUM(cost_usd), 0) AS cost_usd
17927
18635
  FROM ${daily ? "usage_daily" : "request_logs"}
17928
18636
  WHERE ${daily ? "" : "state = 'done' AND "}${timeColumn} >= ? AND ${timeColumn} <= ?
@@ -17936,6 +18644,8 @@ function createUsageRepo(db) {
17936
18644
  outputTokens: r.output_tokens,
17937
18645
  cacheReadTokens: r.cache_read_tokens,
17938
18646
  cacheWriteTokens: r.cache_write_tokens,
18647
+ rtkSavedTokens: r.rtk_saved_tokens,
18648
+ rtkAppliedRequests: r.rtk_applied_requests,
17939
18649
  costUsd: r.cost_usd,
17940
18650
  errors: r.errors,
17941
18651
  durationMsSum: r.duration_ms_sum
@@ -18080,6 +18790,11 @@ async function putModel(store, id, input) {
18080
18790
  if (model.id !== id) {
18081
18791
  throw new GatewayError("BAD_REQUEST", "model id in the path and body must match");
18082
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
+ }
18083
18798
  await store.config.putModel(model);
18084
18799
  }
18085
18800
  async function removeModel(store, id) {
@@ -18202,7 +18917,7 @@ var AUTHORIZE_URL2 = "https://auth.openai.com/oauth/authorize";
18202
18917
  var TOKEN_URL3 = "https://auth.openai.com/oauth/token";
18203
18918
  var SCOPES2 = "openid profile email offline_access";
18204
18919
  var USAGE_URL3 = "https://chatgpt.com/backend-api/wham/usage";
18205
- function isRecord2(value) {
18920
+ function isRecord3(value) {
18206
18921
  return typeof value === "object" && value !== null;
18207
18922
  }
18208
18923
  function nonBlankStringOrNull(value) {
@@ -18219,19 +18934,19 @@ function decodeClaims(idToken) {
18219
18934
  }
18220
18935
  try {
18221
18936
  const json5 = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
18222
- if (!isRecord2(json5))
18937
+ if (!isRecord3(json5))
18223
18938
  return { email: null, accountId: null };
18224
18939
  const auth = json5["https://api.openai.com/auth"];
18225
18940
  return {
18226
18941
  email: typeof json5.email === "string" ? json5.email : null,
18227
- accountId: isRecord2(auth) ? nonBlankStringOrNull(auth.chatgpt_account_id) : null
18942
+ accountId: isRecord3(auth) ? nonBlankStringOrNull(auth.chatgpt_account_id) : null
18228
18943
  };
18229
18944
  } catch {
18230
18945
  return { email: null, accountId: null };
18231
18946
  }
18232
18947
  }
18233
18948
  function parseTokenResponse(value) {
18234
- if (!isRecord2(value) || typeof value.access_token !== "string")
18949
+ if (!isRecord3(value) || typeof value.access_token !== "string")
18235
18950
  return null;
18236
18951
  return {
18237
18952
  accessToken: value.access_token,
@@ -18366,6 +19081,9 @@ function createRefresher(deps) {
18366
19081
  throw new GatewayError("AUTH", `credential ${credential.id} has no refresh token`);
18367
19082
  }
18368
19083
  const provider = deps.providers[credential.provider];
19084
+ if (provider === undefined) {
19085
+ throw new GatewayError("BAD_REQUEST", "provider does not support OAuth refresh");
19086
+ }
18369
19087
  logger2.debug("refreshing credential", {
18370
19088
  provider: credential.provider,
18371
19089
  credentialId: credential.id
@@ -18436,33 +19154,77 @@ async function describeModelsForSetup(store) {
18436
19154
  label: modelDisplayName(model)
18437
19155
  }));
18438
19156
  }
18439
- function slug(id) {
18440
- return encodeURIComponent(id).replace(/\./g, "%2E");
18441
- }
18442
- function claudeProfiles(described, input) {
18443
- const apiKey = input.apiKey ?? KEY_PLACEHOLDER;
18444
- return described.map(({ model, limits }) => {
18445
- const useMirror = input.discoveryMirrors === true && !/^(?:claude|anthropic)/i.test(model.id);
18446
- const modelId = useMirror ? `claude/${model.id}` : model.id;
18447
- const env2 = {
18448
- ANTHROPIC_BASE_URL: input.baseUrl,
18449
- ANTHROPIC_AUTH_TOKEN: apiKey,
18450
- ANTHROPIC_MODEL: modelId,
18451
- CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
18452
- };
18453
- if (limits.contextWindow !== undefined && !/^claude-/i.test(modelId)) {
18454
- env2.CLAUDE_CODE_MAX_CONTEXT_TOKENS = String(limits.contextWindow);
19157
+ var CLAUDE_MAPPING_KEYS = {
19158
+ defaultModel: "ANTHROPIC_MODEL",
19159
+ fableModel: "ANTHROPIC_DEFAULT_FABLE_MODEL",
19160
+ opusModel: "ANTHROPIC_DEFAULT_OPUS_MODEL",
19161
+ sonnetModel: "ANTHROPIC_DEFAULT_SONNET_MODEL",
19162
+ haikuModel: "ANTHROPIC_DEFAULT_HAIKU_MODEL"
19163
+ };
19164
+ function settingsObject(existing) {
19165
+ if (existing === undefined)
19166
+ return {};
19167
+ try {
19168
+ const parsed = JSON.parse(existing);
19169
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
19170
+ throw new Error("settings root is not an object");
18455
19171
  }
18456
- return {
18457
- path: `${slug(model.id)}/settings.json`,
18458
- contents: `${JSON.stringify({ env: env2 }, null, 2)}
19172
+ return parsed;
19173
+ } catch (error51) {
19174
+ const reason = error51 instanceof Error ? error51.message : "invalid JSON";
19175
+ throw new Error(`cannot parse existing settings.json: ${reason}`);
19176
+ }
19177
+ }
19178
+ function claudeSettings(described, input, mapping, existing) {
19179
+ if (mapping.defaultModel === "")
19180
+ throw new Error("default model is required");
19181
+ const ids = new Set(described.map(({ model }) => model.id));
19182
+ const visibleId = (slot, id) => {
19183
+ if (!ids.has(id))
19184
+ throw new Error(`${slot} names unknown virtual model "${id}"`);
19185
+ const useMirror = input.discoveryMirrors === true && !/^(?:claude|anthropic)/i.test(id);
19186
+ return useMirror ? `claude/${id}` : id;
19187
+ };
19188
+ const settings = settingsObject(existing);
19189
+ const currentEnv = settings.env;
19190
+ const env2 = typeof currentEnv === "object" && currentEnv !== null && !Array.isArray(currentEnv) ? { ...currentEnv } : {};
19191
+ for (const key of Object.values(CLAUDE_MAPPING_KEYS))
19192
+ delete env2[key];
19193
+ delete env2.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
19194
+ env2.ANTHROPIC_BASE_URL = input.baseUrl;
19195
+ env2.ANTHROPIC_AUTH_TOKEN = input.apiKey ?? KEY_PLACEHOLDER;
19196
+ env2.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
19197
+ for (const [slot, key] of Object.entries(CLAUDE_MAPPING_KEYS)) {
19198
+ const id = mapping[slot];
19199
+ if (id !== undefined && id !== "")
19200
+ env2[key] = visibleId(slot, id);
19201
+ }
19202
+ return {
19203
+ path: "settings.json",
19204
+ contents: `${JSON.stringify({ ...settings, env: env2 }, null, 2)}
18459
19205
  `
18460
- };
18461
- });
19206
+ };
18462
19207
  }
18463
- function opencodeConfig(described, input) {
19208
+ function opencodeConfig(described, input, mapping) {
19209
+ if (mapping.defaultModel === "")
19210
+ throw new Error("default model is required");
19211
+ const byId = new Map(described.map((entry) => [entry.model.id, entry]));
19212
+ const selected = [];
19213
+ const seen = new Set;
19214
+ for (const slot of Object.keys(CLAUDE_MAPPING_KEYS)) {
19215
+ const id = mapping[slot];
19216
+ if (id === undefined || id === "")
19217
+ continue;
19218
+ const entry = byId.get(id);
19219
+ if (entry === undefined)
19220
+ throw new Error(`${slot} names unknown virtual model "${id}"`);
19221
+ if (!seen.has(id)) {
19222
+ seen.add(id);
19223
+ selected.push(entry);
19224
+ }
19225
+ }
18464
19226
  const models = {};
18465
- for (const { model, limits, label: label2 } of described) {
19227
+ for (const { model, limits, label: label2 } of selected) {
18466
19228
  const limit = limits.contextWindow === undefined ? undefined : {
18467
19229
  context: limits.contextWindow,
18468
19230
  ...limits.maxOutputTokens === undefined ? {} : { output: limits.maxOutputTokens }
@@ -18471,6 +19233,7 @@ function opencodeConfig(described, input) {
18471
19233
  }
18472
19234
  const contents = `${JSON.stringify({
18473
19235
  $schema: "https://opencode.ai/config.json",
19236
+ model: `omnigateway/${mapping.defaultModel}`,
18474
19237
  provider: {
18475
19238
  omnigateway: {
18476
19239
  npm: "@ai-sdk/openai-compatible",
@@ -19040,7 +19803,8 @@ var console_ = {
19040
19803
  var PROVIDER_TONE = {
19041
19804
  anthropic: "magenta",
19042
19805
  openai: "green",
19043
- kimi: "blue"
19806
+ kimi: "blue",
19807
+ custom: "cyan"
19044
19808
  };
19045
19809
  function provider(ctx, id) {
19046
19810
  return paint(ctx, PROVIDER_TONE[id], id);
@@ -19204,21 +19968,36 @@ var credentialsRefresh = {
19204
19968
  }
19205
19969
  };
19206
19970
  var credentialsAddKey = {
19207
- 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]",
19208
19972
  summary: "Store a provider API key, read from a prompt or stdin",
19209
- 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
+ },
19210
19980
  async run(args, { ctx, writer, prompt }) {
19211
19981
  const providerId = requirePositional(args, 0, "provider");
19212
19982
  if (!isProviderId(providerId)) {
19213
- throw new UsageError("provider must be one of anthropic, openai, kimi");
19983
+ throw new UsageError("provider must be one of anthropic, openai, kimi, custom");
19214
19984
  }
19985
+ const protocolFlag = stringFlag(args.values, "protocol");
19986
+ const protocol = protocolFlag === "chat-completions" ? "chat_completions" : protocolFlag === "responses" ? "responses" : protocolFlag;
19215
19987
  const key = await prompt.secret(`${providerId} API key: `);
19216
19988
  if (key.length === 0)
19217
19989
  throw new CliError("no API key given");
19218
- 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, {
19219
19994
  provider: providerId,
19220
19995
  apiKey: key,
19221
- 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
19222
20001
  });
19223
20002
  emit(ctx, writer, { id: created.id, provider: created.provider }, () => `stored ${created.provider} api key as ${created.id}`);
19224
20003
  }
@@ -19867,7 +20646,7 @@ var settingsSet = {
19867
20646
  };
19868
20647
 
19869
20648
  // apps/cli/src/commands/setup.ts
19870
- import { dirname as dirname2, join as join4 } from "path";
20649
+ import { join as join4 } from "path";
19871
20650
  var OPTIONS = {
19872
20651
  dir: { type: "string" },
19873
20652
  key: { type: "string" },
@@ -19901,39 +20680,65 @@ ${f.contents}`).join(`
19901
20680
  function at(dir, file2) {
19902
20681
  return { path: join4(dir, file2.path), contents: file2.contents };
19903
20682
  }
20683
+ async function promptMapping(models, prompt) {
20684
+ const choices = models.map(({ model }) => model.id).join(", ");
20685
+ if (prompt.input === undefined) {
20686
+ throw new CliError("model mapping requires an interactive terminal");
20687
+ }
20688
+ const ask = async (label2, required2) => {
20689
+ const answer = await prompt.input?.(`${label2} model${required2 ? "" : " (blank to omit)"} [${choices}]: `);
20690
+ if (required2 && answer === "")
20691
+ throw new CliError("default model is required");
20692
+ return answer === "" ? undefined : answer;
20693
+ };
20694
+ const defaultModel = await ask("Default", true);
20695
+ if (defaultModel === undefined)
20696
+ throw new CliError("default model is required");
20697
+ const fableModel = await ask("Fable", false);
20698
+ const opusModel = await ask("Opus", false);
20699
+ const sonnetModel = await ask("Sonnet", false);
20700
+ const haikuModel = await ask("Haiku", false);
20701
+ return {
20702
+ defaultModel,
20703
+ ...fableModel === undefined ? {} : { fableModel },
20704
+ ...opusModel === undefined ? {} : { opusModel },
20705
+ ...sonnetModel === undefined ? {} : { sonnetModel },
20706
+ ...haikuModel === undefined ? {} : { haikuModel }
20707
+ };
20708
+ }
19904
20709
  var setupClaude = {
19905
20710
  usage: "setup claude [--dir <path>] [--key <key>] [--dry-run]",
19906
- summary: "Write a Claude Code profile per virtual model",
20711
+ summary: "Write Claude Code settings for this gateway",
19907
20712
  options: OPTIONS,
19908
- async run(args, { ctx, writer, setupFs }) {
20713
+ async run(args, { ctx, writer, prompt, setupFs }) {
19909
20714
  const models = await described(ctx);
19910
- const dir = stringFlag(args.values, "dir") ?? join4(setupFs.homeDir, ".claude", "profiles");
20715
+ const dir = stringFlag(args.values, "dir") ?? join4(setupFs.homeDir, ".claude");
19911
20716
  const key = stringFlag(args.values, "key");
19912
20717
  const dryRun2 = boolFlag(args.values, "dry-run");
19913
- const files = claudeProfiles(models, {
20718
+ const mapping = await promptMapping(models, prompt);
20719
+ const path = join4(dir, "settings.json");
20720
+ const file2 = claudeSettings(models, {
19914
20721
  baseUrl: baseUrl(ctx),
19915
20722
  discoveryMirrors: ctx.config().exposeClaudeCodeAliases,
19916
20723
  ...key === undefined ? {} : { apiKey: key }
19917
- }).map((file2) => at(dir, file2));
19918
- finish(ctx, writer, files, dryRun2, key, setupFs.write);
19919
- if (!dryRun2) {
19920
- note(ctx, writer, `run one with: CLAUDE_CONFIG_DIR=${dirname2(files[0]?.path ?? dir)} claude`);
19921
- }
20724
+ }, mapping, setupFs.read(path) ?? undefined);
20725
+ finish(ctx, writer, [at(dir, file2)], dryRun2, key, setupFs.write);
19922
20726
  }
19923
20727
  };
19924
20728
  var setupOpencode = {
19925
20729
  usage: "setup opencode [--dir <path>] [--key <key>] [--dry-run]",
19926
20730
  summary: "Write an opencode.json provider entry for this gateway",
19927
20731
  options: OPTIONS,
19928
- async run(args, { ctx, writer, setupFs }) {
20732
+ async run(args, { ctx, writer, prompt, setupFs }) {
19929
20733
  const models = await described(ctx);
19930
20734
  const dir = stringFlag(args.values, "dir") ?? setupFs.cwd;
19931
20735
  const key = stringFlag(args.values, "key");
19932
20736
  const dryRun2 = boolFlag(args.values, "dry-run");
20737
+ const mapping = await promptMapping(models, prompt);
19933
20738
  const file2 = opencodeConfig(models, {
19934
20739
  baseUrl: baseUrl(ctx),
19935
20740
  ...key === undefined ? {} : { apiKey: key }
19936
- });
20741
+ }, mapping);
19937
20742
  finish(ctx, writer, [at(dir, file2)], dryRun2, key, setupFs.write);
19938
20743
  }
19939
20744
  };
@@ -20316,6 +21121,12 @@ function createPrompt(ctx, writer) {
20316
21121
  const isTty = process.stdin.isTTY === true;
20317
21122
  return {
20318
21123
  isTty,
21124
+ async input(question) {
21125
+ if (!isTty)
21126
+ throw new CliError(`${question} \u2014 refusing without a terminal`);
21127
+ writer.err(question);
21128
+ return (await readLine()).trim();
21129
+ },
20319
21130
  async secret(question) {
20320
21131
  if (!isTty)
20321
21132
  return (await readAllStdin()).trim();
@@ -20336,6 +21147,20 @@ function createPrompt(ctx, writer) {
20336
21147
  };
20337
21148
  }
20338
21149
 
21150
+ // apps/cli/src/setupFs.ts
21151
+ import { dirname as dirname2 } from "path";
21152
+ function atomicWriteFile(path, contents, ops) {
21153
+ const temporary = `${path}.${process.pid}.tmp`;
21154
+ ops.mkdir(dirname2(path));
21155
+ try {
21156
+ ops.write(temporary, contents);
21157
+ ops.rename(temporary, path);
21158
+ } catch (error51) {
21159
+ ops.remove(temporary);
21160
+ throw error51;
21161
+ }
21162
+ }
21163
+
20339
21164
  // apps/cli/src/run.ts
20340
21165
  var VERSION = "0.0.0";
20341
21166
  async function run(argv, writer, options = {}) {
@@ -20377,10 +21202,13 @@ async function run(argv, writer, options = {}) {
20377
21202
  setupFs: options.setupFs ?? {
20378
21203
  homeDir: homedir3(),
20379
21204
  cwd: process.cwd(),
20380
- write: (path, contents) => {
20381
- mkdirSync2(dirname3(path), { recursive: true });
20382
- writeFileSync2(path, contents);
20383
- }
21205
+ read: (path) => existsSync6(path) ? readFileSync3(path, "utf8") : null,
21206
+ write: (path, contents) => atomicWriteFile(path, contents, {
21207
+ mkdir: (directory) => mkdirSync2(directory, { recursive: true }),
21208
+ write: (temporary, data) => writeFileSync2(temporary, data),
21209
+ rename: renameSync,
21210
+ remove: (temporary) => rmSync2(temporary, { force: true })
21211
+ })
20384
21212
  },
20385
21213
  service: () => options.service?.({ root: ctx.root.root, env: ctx.env }) ?? createServiceDeps({ root: ctx.root.root, env: ctx.env, scope, now: ctx.now }),
20386
21214
  foreground: options.foreground ?? runForeground,