omnigateway 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/omni.js CHANGED
@@ -147,35 +147,6 @@ function createAdminAuth(store, opts) {
147
147
  }
148
148
  };
149
149
  }
150
- // packages/control/src/config.ts
151
- var MIN_KEY_LENGTH = 16;
152
- var DECIMAL_INTEGER = /^\d+$/;
153
- function optionalText(value, fallback) {
154
- return value?.trim() || fallback;
155
- }
156
- function loadConfig(env) {
157
- const encryptionKey = env.OMNI_ENCRYPTION_KEY;
158
- if (typeof encryptionKey !== "string" || encryptionKey.length < MIN_KEY_LENGTH) {
159
- throw new Error(`OMNI_ENCRYPTION_KEY must be set to at least ${MIN_KEY_LENGTH} characters. ` + "Generate one with: openssl rand -base64 32");
160
- }
161
- const host = optionalText(env.OMNI_HOST, "127.0.0.1");
162
- const rawPort = env.OMNI_PORT ?? "9000";
163
- const port = Number(rawPort);
164
- if (!DECIMAL_INTEGER.test(rawPort) || !Number.isInteger(port) || port < 1 || port > 65535) {
165
- throw new Error(`OMNI_PORT must be an integer between 1 and 65535, got "${rawPort}"`);
166
- }
167
- const derivedBaseUrl = `http://${host}:${port}`;
168
- const baseUrl = optionalText(env.OMNI_BASE_URL, derivedBaseUrl).replace(/\/+$/, "") || derivedBaseUrl;
169
- const staticDir = env.OMNI_STATIC_DIR?.trim();
170
- return {
171
- port,
172
- host,
173
- databasePath: optionalText(env.OMNI_DB_PATH, "./omnigateway.db"),
174
- encryptionKey,
175
- baseUrl,
176
- staticDir: staticDir === undefined || staticDir.length === 0 ? null : staticDir
177
- };
178
- }
179
150
  // packages/ir/src/capabilities.ts
180
151
  var PROVIDER_CAPABILITIES = {
181
152
  anthropic: { tools: true, images: true, reasoning: true },
@@ -215,6 +186,67 @@ class GatewayError extends Error {
215
186
  this.retryAfterMs = opts?.retryAfterMs;
216
187
  }
217
188
  }
189
+ // packages/ir/src/logger.ts
190
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
191
+ var noopLogger = {
192
+ debug: () => {},
193
+ info: () => {},
194
+ warn: () => {},
195
+ error: () => {},
196
+ enabled: () => false
197
+ };
198
+ function parseLogLevel(value) {
199
+ const normalized = value?.trim().toLowerCase();
200
+ if (normalized === undefined || normalized.length === 0)
201
+ return null;
202
+ return Object.hasOwn(LEVELS, normalized) ? normalized : null;
203
+ }
204
+ // packages/ir/src/request.ts
205
+ function cacheControlOf(block) {
206
+ return block.type === "thinking" ? undefined : block.cacheControl;
207
+ }
208
+ // packages/ir/src/stream.ts
209
+ function usageFromPromptTotal(promptTokens, outputTokens, cacheReadTokens, cacheWriteTokens = 0) {
210
+ return {
211
+ inputTokens: Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens),
212
+ outputTokens,
213
+ cacheReadTokens,
214
+ cacheWriteTokens
215
+ };
216
+ }
217
+ // packages/control/src/config.ts
218
+ var MIN_KEY_LENGTH = 16;
219
+ var DECIMAL_INTEGER = /^\d+$/;
220
+ function optionalText(value, fallback) {
221
+ return value?.trim() || fallback;
222
+ }
223
+ function loadConfig(env) {
224
+ const encryptionKey = env.OMNI_ENCRYPTION_KEY;
225
+ if (typeof encryptionKey !== "string" || encryptionKey.length < MIN_KEY_LENGTH) {
226
+ throw new Error(`OMNI_ENCRYPTION_KEY must be set to at least ${MIN_KEY_LENGTH} characters. ` + "Generate one with: openssl rand -base64 32");
227
+ }
228
+ const host = optionalText(env.OMNI_HOST, "127.0.0.1");
229
+ const rawPort = env.OMNI_PORT ?? "9000";
230
+ const port = Number(rawPort);
231
+ if (!DECIMAL_INTEGER.test(rawPort) || !Number.isInteger(port) || port < 1 || port > 65535) {
232
+ throw new Error(`OMNI_PORT must be an integer between 1 and 65535, got "${rawPort}"`);
233
+ }
234
+ const derivedBaseUrl = `http://${host}:${port}`;
235
+ const baseUrl = optionalText(env.OMNI_BASE_URL, derivedBaseUrl).replace(/\/+$/, "") || derivedBaseUrl;
236
+ const staticDir = env.OMNI_STATIC_DIR?.trim();
237
+ const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
238
+ const logLevel = parseLogLevel(rawLogLevel);
239
+ return {
240
+ logLevel: logLevel ?? "info",
241
+ logLevelFallbackFrom: logLevel === null && rawLogLevel ? rawLogLevel : null,
242
+ port,
243
+ host,
244
+ databasePath: optionalText(env.OMNI_DB_PATH, "./omnigateway.db"),
245
+ encryptionKey,
246
+ baseUrl,
247
+ staticDir: staticDir === undefined || staticDir.length === 0 ? null : staticDir
248
+ };
249
+ }
218
250
  // packages/providers/src/body.ts
219
251
  var BODY_ORDER = {
220
252
  anthropic: [
@@ -301,8 +333,13 @@ function applyAnthropicSystem(system) {
301
333
  let rewritten = text;
302
334
  for (const [from, to] of REWRITES)
303
335
  rewritten = rewritten.replaceAll(from, to);
304
- if (rewritten.trim().length > 0)
305
- kept.push({ type: "text", text: rewritten });
336
+ if (rewritten.trim().length > 0) {
337
+ kept.push({
338
+ type: "text",
339
+ text: rewritten,
340
+ ...block.cache_control === undefined ? {} : { cache_control: block.cache_control }
341
+ });
342
+ }
306
343
  }
307
344
  return [{ type: "text", text: billingBlock() }, { type: "text", text: AGENT_PREAMBLE }, ...kept];
308
345
  }
@@ -606,6 +643,8 @@ async function* decodeAnthropic(messages) {
606
643
  let inputTokens = 0;
607
644
  let cacheReadTokens = 0;
608
645
  let cacheWriteTokens = 0;
646
+ let cacheWrite5mTokens;
647
+ let cacheWrite1hTokens;
609
648
  let outputTokens = 0;
610
649
  let stopReason = "endTurn";
611
650
  let terminal = false;
@@ -619,6 +658,8 @@ async function* decodeAnthropic(messages) {
619
658
  inputTokens = m.usage?.input_tokens ?? 0;
620
659
  cacheReadTokens = m.usage?.cache_read_input_tokens ?? 0;
621
660
  cacheWriteTokens = m.usage?.cache_creation_input_tokens ?? 0;
661
+ cacheWrite5mTokens = m.usage?.cache_creation?.ephemeral_5m_input_tokens;
662
+ cacheWrite1hTokens = m.usage?.cache_creation?.ephemeral_1h_input_tokens;
622
663
  yield { type: "start", id: String(m.id ?? ""), model: String(m.model ?? "") };
623
664
  break;
624
665
  }
@@ -678,7 +719,14 @@ async function* decodeAnthropic(messages) {
678
719
  yield {
679
720
  type: "end",
680
721
  stopReason,
681
- usage: { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens }
722
+ usage: {
723
+ inputTokens,
724
+ outputTokens,
725
+ cacheReadTokens,
726
+ cacheWriteTokens,
727
+ ...cacheWrite5mTokens === undefined ? {} : { cacheWrite5mTokens },
728
+ ...cacheWrite1hTokens === undefined ? {} : { cacheWrite1hTokens }
729
+ }
682
730
  };
683
731
  break;
684
732
  case "error": {
@@ -708,25 +756,33 @@ async function* decodeAnthropic(messages) {
708
756
 
709
757
  // packages/providers/src/anthropic/wire.ts
710
758
  var OAUTH_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
759
+ function wireCacheControl(c) {
760
+ if (c === undefined)
761
+ return {};
762
+ return { cache_control: { type: c.type, ...c.ttl === undefined ? {} : { ttl: c.ttl } } };
763
+ }
711
764
  function encodeBlock(b) {
765
+ const cache = wireCacheControl(cacheControlOf(b));
712
766
  switch (b.type) {
713
767
  case "text":
714
- return { type: "text", text: b.text };
768
+ return { type: "text", text: b.text, ...cache };
715
769
  case "image":
716
770
  return {
717
771
  type: "image",
718
- source: { type: "base64", media_type: b.mediaType, data: b.data }
772
+ source: { type: "base64", media_type: b.mediaType, data: b.data },
773
+ ...cache
719
774
  };
720
775
  case "thinking":
721
776
  return { type: "thinking", thinking: b.text, signature: b.signature };
722
777
  case "toolUse":
723
- return { type: "tool_use", id: b.id, name: b.name, input: b.input };
778
+ return { type: "tool_use", id: b.id, name: b.name, input: b.input, ...cache };
724
779
  case "toolResult":
725
780
  return {
726
781
  type: "tool_result",
727
782
  tool_use_id: b.toolUseId,
728
783
  content: b.content,
729
- is_error: b.isError
784
+ is_error: b.isError,
785
+ ...cache
730
786
  };
731
787
  }
732
788
  }
@@ -748,14 +804,25 @@ function encodeToolChoice(c) {
748
804
  }
749
805
  function toWire(req, model, opts) {
750
806
  const degradations = [];
751
- let system = req.system?.flatMap((b) => b.type === "text" ? [{ type: "text", text: b.text }] : []);
807
+ const note2 = (d) => {
808
+ if (!degradations.includes(d))
809
+ degradations.push(d);
810
+ };
811
+ let system = req.system?.flatMap((b) => b.type === "text" ? [{ type: "text", text: b.text, ...wireCacheControl(b.cacheControl) }] : []);
752
812
  if (opts.oauth && system?.[0]?.text !== OAUTH_IDENTITY) {
753
813
  system = [{ type: "text", text: OAUTH_IDENTITY }, ...system ?? []];
754
- degradations.push("anthropic:oauth-system-prefix");
814
+ note2("anthropic:oauth-system-prefix");
755
815
  }
756
816
  const body = {
757
817
  model,
758
- messages: req.messages.map((m) => m.role === "system" ? { role: m.role, content: encodeSystemTurn(m.content) } : { role: m.role, content: m.content.map(encodeBlock) }),
818
+ messages: req.messages.map((m) => {
819
+ if (m.role !== "system")
820
+ return { role: m.role, content: m.content.map(encodeBlock) };
821
+ if (m.content.some((b) => cacheControlOf(b) !== undefined)) {
822
+ note2("anthropic:system-turn-cache-control-dropped");
823
+ }
824
+ return { role: m.role, content: encodeSystemTurn(m.content) };
825
+ }),
759
826
  max_tokens: req.maxTokens ?? 4096,
760
827
  stream: req.stream
761
828
  };
@@ -769,7 +836,8 @@ function toWire(req, model, opts) {
769
836
  body.tools = req.tools.map((t) => ({
770
837
  name: t.name,
771
838
  ...t.description === undefined ? {} : { description: t.description },
772
- input_schema: t.inputSchema
839
+ input_schema: t.inputSchema,
840
+ ...wireCacheControl(t.cacheControl)
773
841
  }));
774
842
  }
775
843
  if (req.toolChoice !== undefined)
@@ -809,12 +877,13 @@ var anthropicAdapter = {
809
877
  const { body, degradations } = toWire(req.request, req.model, { oauth });
810
878
  const withSystem = {
811
879
  ...body,
812
- system: applyAnthropicSystem(body.system ?? [])
880
+ system: applyAnthropicSystem(body.system ?? []),
881
+ stream: true
813
882
  };
814
883
  const protocol = [
815
884
  ["Content-Type", "application/json"],
816
885
  ["anthropic-version", API_VERSION],
817
- ["Accept", req.request.stream ? "text/event-stream" : "application/json"]
886
+ ["Accept", "text/event-stream"]
818
887
  ];
819
888
  const betas = new Set(req.request.betas ?? []);
820
889
  if (oauth) {
@@ -833,6 +902,7 @@ var anthropicAdapter = {
833
902
  const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
834
903
  const bodyString = signAnthropicBody(JSON.stringify(orderFields(withSystem, BODY_ORDER.anthropic)));
835
904
  const res = await req.http({
905
+ provider: "anthropic",
836
906
  url: BASE_URL,
837
907
  method: "POST",
838
908
  headers,
@@ -853,22 +923,26 @@ var ANTHROPIC_MODELS = {
853
923
  {
854
924
  id: "claude-fable-5",
855
925
  label: "Claude Fable 5",
856
- pricing: { input: 10, output: 50, cacheRead: 1 }
926
+ pricing: { input: 10, output: 50, cacheRead: 1, cacheWrite5m: 12.5, cacheWrite1h: 20 },
927
+ limits: { contextWindow: 1e6, maxOutputTokens: 128000 }
857
928
  },
858
929
  {
859
930
  id: "claude-opus-5",
860
931
  label: "Claude Opus 5",
861
- pricing: { input: 5, output: 25, cacheRead: 0.5 }
932
+ pricing: { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
933
+ limits: { contextWindow: 1e6, maxOutputTokens: 128000 }
862
934
  },
863
935
  {
864
936
  id: "claude-sonnet-5",
865
937
  label: "Claude Sonnet 5",
866
- pricing: { input: 3, output: 15, cacheRead: 0.3 }
938
+ pricing: { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 3.75, cacheWrite1h: 6 },
939
+ limits: { contextWindow: 1e6, maxOutputTokens: 128000 }
867
940
  },
868
941
  {
869
942
  id: "claude-haiku-4-5",
870
943
  label: "Claude Haiku 4.5",
871
- pricing: { input: 1, output: 5, cacheRead: 0.1 }
944
+ pricing: { input: 1, output: 5, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2 },
945
+ limits: { contextWindow: 200000, maxOutputTokens: 64000 }
872
946
  }
873
947
  ]
874
948
  };
@@ -880,22 +954,26 @@ var KIMI_MODELS = {
880
954
  {
881
955
  id: "k3-256k",
882
956
  label: "Kimi K3 \u2014 256K",
883
- pricing: { input: 3, output: 15, cacheRead: 0.3 }
957
+ pricing: { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 0, cacheWrite1h: 0 },
958
+ limits: { contextWindow: 262144, maxOutputTokens: 131072 }
884
959
  },
885
960
  {
886
961
  id: "k3",
887
962
  label: "Kimi K3 \u2014 up to 1M",
888
- pricing: { input: 3, output: 15, cacheRead: 0.3 }
963
+ pricing: { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 0, cacheWrite1h: 0 },
964
+ limits: { contextWindow: 1048576, maxOutputTokens: 131072 }
889
965
  },
890
966
  {
891
967
  id: "kimi-for-coding",
892
968
  label: "Kimi K2.7 Code",
893
- pricing: { input: 0.95, output: 4, cacheRead: 0.19 }
969
+ pricing: { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite5m: 0, cacheWrite1h: 0 },
970
+ limits: { contextWindow: 262144, maxOutputTokens: 131072 }
894
971
  },
895
972
  {
896
973
  id: "kimi-for-coding-highspeed",
897
974
  label: "Kimi K2.7 Code \u2014 High Speed",
898
- pricing: { input: 0.95, output: 8, cacheRead: 0.19 }
975
+ pricing: { input: 0.95, output: 8, cacheRead: 0.19, cacheWrite5m: 0, cacheWrite1h: 0 },
976
+ limits: { contextWindow: 262144, maxOutputTokens: 131072 }
899
977
  }
900
978
  ]
901
979
  };
@@ -907,22 +985,30 @@ var OPENAI_MODELS = {
907
985
  {
908
986
  id: "gpt-5.6",
909
987
  label: "GPT-5.6 \u2014 routes to Sol",
910
- pricing: { input: 5, output: 30, cacheRead: 0.5 }
988
+ pricing: { input: 5, output: 30, cacheRead: 0.5, cacheWrite5m: 0, cacheWrite1h: 0 },
989
+ limits: { contextWindow: 922000, maxOutputTokens: 128000 },
990
+ oauthLimits: { contextWindow: 272000, maxOutputTokens: 128000 }
911
991
  },
912
992
  {
913
993
  id: "gpt-5.6-sol",
914
994
  label: "GPT-5.6 Sol \u2014 deepest reasoning",
915
- pricing: { input: 5, output: 30, cacheRead: 0.5 }
995
+ pricing: { input: 5, output: 30, cacheRead: 0.5, cacheWrite5m: 0, cacheWrite1h: 0 },
996
+ limits: { contextWindow: 922000, maxOutputTokens: 128000 },
997
+ oauthLimits: { contextWindow: 272000, maxOutputTokens: 128000 }
916
998
  },
917
999
  {
918
1000
  id: "gpt-5.6-terra",
919
1001
  label: "GPT-5.6 Terra \u2014 balanced",
920
- pricing: { input: 2, output: 12, cacheRead: 0.2 }
1002
+ pricing: { input: 2, output: 12, cacheRead: 0.2, cacheWrite5m: 0, cacheWrite1h: 0 },
1003
+ limits: { contextWindow: 922000, maxOutputTokens: 128000 },
1004
+ oauthLimits: { contextWindow: 272000, maxOutputTokens: 128000 }
921
1005
  },
922
1006
  {
923
1007
  id: "gpt-5.6-luna",
924
1008
  label: "GPT-5.6 Luna \u2014 fastest",
925
- pricing: { input: 0.2, output: 1.2, cacheRead: 0.02 }
1009
+ pricing: { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite5m: 0, cacheWrite1h: 0 },
1010
+ limits: { contextWindow: 922000, maxOutputTokens: 128000 },
1011
+ oauthLimits: { contextWindow: 272000, maxOutputTokens: 128000 }
926
1012
  }
927
1013
  ]
928
1014
  };
@@ -934,15 +1020,38 @@ var PROVIDER_MODEL_CATALOG = {
934
1020
  kimi: KIMI_MODELS
935
1021
  };
936
1022
  function catalogPricing(provider, model) {
937
- return PROVIDER_MODEL_CATALOG[provider].models.find((entry) => entry.id === model)?.pricing ?? null;
1023
+ return PROVIDER_MODEL_CATALOG[provider]?.models.find((entry) => entry.id === model)?.pricing ?? null;
1024
+ }
1025
+ function catalogLimits(provider, model, auth = "apiKey") {
1026
+ const entry = PROVIDER_MODEL_CATALOG[provider]?.models.find((choice) => choice.id === model);
1027
+ if (entry === undefined)
1028
+ return null;
1029
+ return (auth === "oauth" ? entry.oauthLimits : undefined) ?? entry.limits;
938
1030
  }
939
1031
  // packages/providers/src/http-client.ts
940
1032
  import { request as httpRequest } from "http";
941
1033
  import { request as httpsRequest } from "https";
942
1034
  import { Readable } from "stream";
943
- function nodeHttpClient() {
1035
+ function nodeHttpClient(options = {}) {
1036
+ const logger2 = options.logger ?? noopLogger;
1037
+ const now = options.now ?? (() => Date.now());
944
1038
  return (req) => new Promise((resolve, reject) => {
945
1039
  const url = new URL(req.url);
1040
+ const startedAt = now();
1041
+ let traced = false;
1042
+ const trace = (status, failed = false) => {
1043
+ if (traced || !logger2.enabled("debug"))
1044
+ return;
1045
+ traced = true;
1046
+ logger2.debug("upstream http", {
1047
+ provider: req.provider,
1048
+ status,
1049
+ host: url.host,
1050
+ path: url.pathname,
1051
+ durationMs: now() - startedAt,
1052
+ reason: failed ? "transport error" : undefined
1053
+ });
1054
+ };
946
1055
  const send = url.protocol === "https:" ? httpsRequest : httpRequest;
947
1056
  const bodyBytes = Buffer.from(req.body, "utf8");
948
1057
  const headers = {};
@@ -970,6 +1079,7 @@ function nodeHttpClient() {
970
1079
  else if (typeof v === "string")
971
1080
  responseHeaders.set(k, v);
972
1081
  }
1082
+ trace(incoming.statusCode);
973
1083
  resolve({
974
1084
  status: incoming.statusCode ?? 0,
975
1085
  headers: responseHeaders,
@@ -985,17 +1095,17 @@ function nodeHttpClient() {
985
1095
  });
986
1096
  });
987
1097
  const onAbort = () => outgoing.destroy(new Error("aborted"));
988
- if (req.signal.aborted) {
989
- outgoing.destroy(new Error("aborted"));
990
- reject(new Error("aborted"));
991
- return;
992
- }
993
- req.signal.addEventListener("abort", onAbort, { once: true });
994
1098
  outgoing.on("error", (err) => {
995
1099
  req.signal.removeEventListener("abort", onAbort);
1100
+ trace(undefined, true);
996
1101
  reject(err);
997
1102
  });
998
1103
  outgoing.on("close", () => req.signal.removeEventListener("abort", onAbort));
1104
+ if (req.signal.aborted) {
1105
+ outgoing.destroy(new Error("aborted"));
1106
+ return;
1107
+ }
1108
+ req.signal.addEventListener("abort", onAbort, { once: true });
999
1109
  if (bodyBytes.byteLength > 0)
1000
1110
  outgoing.write(bodyBytes);
1001
1111
  outgoing.end();
@@ -1064,12 +1174,8 @@ async function* decodeChat(messages) {
1064
1174
  yield { type: "start", id: String(d.id ?? ""), model: String(d.model ?? "") };
1065
1175
  }
1066
1176
  if (d.usage) {
1067
- usage = {
1068
- inputTokens: d.usage.prompt_tokens ?? 0,
1069
- outputTokens: d.usage.completion_tokens ?? 0,
1070
- cacheReadTokens: d.usage.prompt_cache_hit_tokens ?? 0,
1071
- cacheWriteTokens: 0
1072
- };
1177
+ const details = d.usage.prompt_tokens_details;
1178
+ usage = usageFromPromptTotal(d.usage.prompt_tokens ?? 0, d.usage.completion_tokens ?? 0, details?.cached_tokens ?? d.usage.prompt_cache_hit_tokens ?? 0, details?.cache_creation_tokens ?? details?.cache_write_tokens ?? 0);
1073
1179
  }
1074
1180
  const choice = d.choices?.[0];
1075
1181
  if (!choice)
@@ -1195,7 +1301,12 @@ function toChatWire(req, model) {
1195
1301
  `) });
1196
1302
  }
1197
1303
  }
1198
- const body = { model, messages, stream: req.stream };
1304
+ const body = {
1305
+ model,
1306
+ messages,
1307
+ stream: req.stream,
1308
+ stream_options: { include_usage: true }
1309
+ };
1199
1310
  if (req.maxTokens !== undefined)
1200
1311
  body.max_tokens = req.maxTokens;
1201
1312
  if (req.temperature !== undefined)
@@ -1236,6 +1347,7 @@ var kimiAdapter = {
1236
1347
  const profile = PROFILES.kimi;
1237
1348
  const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
1238
1349
  const res = await req.http({
1350
+ provider: "kimi",
1239
1351
  url: BASE_URL2,
1240
1352
  method: "POST",
1241
1353
  headers,
@@ -1366,12 +1478,7 @@ async function* decodeResponses(messages) {
1366
1478
  yield {
1367
1479
  type: "end",
1368
1480
  stopReason,
1369
- usage: {
1370
- inputTokens: r.usage?.input_tokens ?? 0,
1371
- outputTokens: r.usage?.output_tokens ?? 0,
1372
- cacheReadTokens: r.usage?.input_tokens_details?.cached_tokens ?? 0,
1373
- cacheWriteTokens: 0
1374
- }
1481
+ usage: usageFromPromptTotal(r.usage?.input_tokens ?? 0, r.usage?.output_tokens ?? 0, r.usage?.input_tokens_details?.cached_tokens ?? r.usage?.prompt_tokens_details?.cached_tokens ?? 0)
1375
1482
  };
1376
1483
  break;
1377
1484
  }
@@ -1544,6 +1651,7 @@ var openaiAdapter = {
1544
1651
  const profile = PROFILES.openai;
1545
1652
  const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
1546
1653
  const res = await req.http({
1654
+ provider: "openai",
1547
1655
  url: oauth ? OAUTH_URL : API_URL,
1548
1656
  method: "POST",
1549
1657
  headers,
@@ -1558,13 +1666,14 @@ var openaiAdapter = {
1558
1666
  }
1559
1667
  };
1560
1668
  // packages/control/src/oauth/types.ts
1561
- async function postJson(deps, url, profile2, opts) {
1669
+ async function postJson(deps, provider, url, profile2, opts) {
1562
1670
  const headers = orderHeaders(mergeHeaders(profile2.headers, [
1563
1671
  ["Content-Type", opts.contentType],
1564
1672
  ["Accept", "application/json"],
1565
1673
  ...opts.extraHeaders ?? []
1566
1674
  ]), profile2.order);
1567
1675
  const res = await deps.http({
1676
+ provider,
1568
1677
  url,
1569
1678
  method: "POST",
1570
1679
  headers,
@@ -1578,13 +1687,14 @@ async function postJson(deps, url, profile2, opts) {
1578
1687
  } catch {}
1579
1688
  return { status: res.status, parsed };
1580
1689
  }
1581
- async function getJson(deps, url, profile2, opts) {
1690
+ async function getJson(deps, provider, url, profile2, opts) {
1582
1691
  const headers = orderHeaders(mergeHeaders(profile2.headers, [
1583
1692
  ["Authorization", `Bearer ${opts.accessToken}`],
1584
1693
  ["Accept", "application/json"],
1585
1694
  ...opts.extraHeaders ?? []
1586
1695
  ]), profile2.order);
1587
1696
  const res = await deps.http({
1697
+ provider,
1588
1698
  url,
1589
1699
  method: "GET",
1590
1700
  headers,
@@ -1778,7 +1888,7 @@ function deviceCodeFrom(value) {
1778
1888
  };
1779
1889
  }
1780
1890
  async function post(url, body2, device, deps) {
1781
- const { status, parsed } = await postJson(deps, url, PROFILES.kimi, {
1891
+ const { status, parsed } = await postJson(deps, "kimi", url, PROFILES.kimi, {
1782
1892
  contentType: "application/x-www-form-urlencoded",
1783
1893
  body: new URLSearchParams({ ...body2, client_id: CLIENT_ID }).toString(),
1784
1894
  extraHeaders: kimiDeviceHeaders(device)
@@ -1881,7 +1991,7 @@ var kimiOAuth = {
1881
1991
  if (secrets.accessToken === null)
1882
1992
  return null;
1883
1993
  const device = deviceFrom(providerData);
1884
- const { status, parsed } = await getJson(deps, USAGE_URL, PROFILES.kimi, {
1994
+ const { status, parsed } = await getJson(deps, "kimi", USAGE_URL, PROFILES.kimi, {
1885
1995
  accessToken: secrets.accessToken,
1886
1996
  extraHeaders: kimiDeviceHeaders(device)
1887
1997
  });
@@ -1950,6 +2060,7 @@ function deviceIdFrom(start) {
1950
2060
  return deviceId;
1951
2061
  }
1952
2062
  function createConnectFlows(deps) {
2063
+ const logger2 = deps.logger ?? noopLogger;
1953
2064
  const flows = createPendingFlows({ now: deps.now, ttlMs: FLOW_TTL_MS });
1954
2065
  const pollsInFlight = new Map;
1955
2066
  const callbackUri = (provider) => provider === "openai" ? "http://localhost:1455/auth/callback" : "";
@@ -1994,6 +2105,10 @@ function createConnectFlows(deps) {
1994
2105
  disabledAt: null,
1995
2106
  ...result.secrets
1996
2107
  });
2108
+ logger2.info("oauth connect completed", {
2109
+ provider: flow.provider,
2110
+ credentialId: id
2111
+ });
1997
2112
  return { id };
1998
2113
  }
1999
2114
  async function pollOutcome(flowId, poll) {
@@ -16369,28 +16484,18 @@ var modelSchema = exports_external.object({
16369
16484
  costPerMTok: exports_external.object({
16370
16485
  input: exports_external.number().min(0),
16371
16486
  output: exports_external.number().min(0),
16372
- cacheRead: exports_external.number().min(0)
16373
- }),
16374
- capabilities: exports_external.object({
16375
- tools: exports_external.boolean(),
16376
- images: exports_external.boolean(),
16377
- reasoning: exports_external.boolean()
16378
- })
16379
- }).or(exports_external.object({
16380
- provider: providerIdSchema,
16381
- model: exports_external.string().min(1),
16382
- tier: exports_external.number().int().min(1),
16383
- weight: exports_external.number().positive(),
16384
- costPerMTok: exports_external.object({
16385
- input: exports_external.number().min(0),
16386
- output: exports_external.number().min(0)
16487
+ cacheRead: exports_external.number().min(0).optional(),
16488
+ cacheWrite5m: exports_external.number().min(0).optional(),
16489
+ cacheWrite1h: exports_external.number().min(0).optional()
16387
16490
  }),
16491
+ contextWindow: exports_external.number().int().positive().optional(),
16492
+ maxOutputTokens: exports_external.number().int().positive().optional(),
16388
16493
  capabilities: exports_external.object({
16389
16494
  tools: exports_external.boolean(),
16390
16495
  images: exports_external.boolean(),
16391
16496
  reasoning: exports_external.boolean()
16392
16497
  })
16393
- }))).min(1, "a virtual model needs at least one target")
16498
+ })).min(1, "a virtual model needs at least one target")
16394
16499
  });
16395
16500
  var keyCreateSchema = exports_external.object({
16396
16501
  label: exports_external.string().min(1).default("api key"),
@@ -16468,7 +16573,7 @@ async function getCredential(store, id) {
16468
16573
  throw new GatewayError("BAD_REQUEST", "no such credential");
16469
16574
  return summarizeCredential(credential);
16470
16575
  }
16471
- async function createApiKeyCredential(store, input) {
16576
+ async function createApiKeyCredential(store, input, logger2 = noopLogger) {
16472
16577
  const provider = parseOrThrow(providerIdSchema, input.provider);
16473
16578
  if (typeof input.apiKey !== "string" || input.apiKey.trim().length === 0) {
16474
16579
  throw new GatewayError("BAD_REQUEST", "apiKey: must not be empty");
@@ -16495,6 +16600,7 @@ async function createApiKeyCredential(store, input) {
16495
16600
  apiKey: input.apiKey,
16496
16601
  idToken: null
16497
16602
  });
16603
+ logger2.info("credential added", { credentialId: created.id, provider: created.provider });
16498
16604
  return summarizeCredential(created);
16499
16605
  }
16500
16606
  async function refreshCredential(deps, id) {
@@ -16554,6 +16660,12 @@ async function patchCredential(deps, id, input) {
16554
16660
  ...patch.tier === undefined ? {} : { tier: patch.tier },
16555
16661
  ...patch.weight === undefined ? {} : { weight: patch.weight }
16556
16662
  });
16663
+ if (patch.enabled !== undefined && patch.enabled !== existing.enabled) {
16664
+ (deps.logger ?? noopLogger).info(patch.enabled ? "credential enabled" : "credential disabled", {
16665
+ credentialId: id,
16666
+ provider: existing.provider
16667
+ });
16668
+ }
16557
16669
  }
16558
16670
  async function removeCredential(store, id) {
16559
16671
  await store.credentials.remove(id);
@@ -17718,7 +17830,9 @@ function createUsageRepo(db) {
17718
17830
 
17719
17831
  // packages/store/src/sqlite/store.ts
17720
17832
  async function createStore(opts) {
17833
+ const logger2 = opts.logger ?? noopLogger;
17721
17834
  const db = openDb(opts.path);
17835
+ logger2.debug("store opened", { path: opts.path });
17722
17836
  const listeners = new Set;
17723
17837
  const emit2 = (change) => {
17724
17838
  for (const listener of listeners) {
@@ -17811,7 +17925,7 @@ var REDIRECT_URI = "https://platform.claude.com/oauth/code/callback";
17811
17925
  var SCOPES = "org:create_api_key user:profile user:inference";
17812
17926
  var USAGE_URL2 = "https://api.anthropic.com/api/oauth/usage";
17813
17927
  async function postToken(body2, deps) {
17814
- const { status, parsed } = await postJson(deps, TOKEN_URL2, PROFILES.anthropic, {
17928
+ const { status, parsed } = await postJson(deps, "anthropic", TOKEN_URL2, PROFILES.anthropic, {
17815
17929
  contentType: "application/json",
17816
17930
  body: JSON.stringify({ ...body2, client_id: CLIENT_ID2 })
17817
17931
  });
@@ -17889,7 +18003,7 @@ var anthropicOAuth = {
17889
18003
  async usage(secrets, deps) {
17890
18004
  if (secrets.accessToken === null)
17891
18005
  return null;
17892
- const { status, parsed } = await getJson(deps, USAGE_URL2, PROFILES.anthropic, {
18006
+ const { status, parsed } = await getJson(deps, "anthropic", USAGE_URL2, PROFILES.anthropic, {
17893
18007
  accessToken: secrets.accessToken,
17894
18008
  extraHeaders: [
17895
18009
  ["anthropic-beta", "oauth-2025-04-20"],
@@ -17947,7 +18061,7 @@ function parseTokenResponse(value) {
17947
18061
  };
17948
18062
  }
17949
18063
  async function postToken2(body2, deps) {
17950
- const { status, parsed } = await postJson(deps, TOKEN_URL3, PROFILES.openai, {
18064
+ const { status, parsed } = await postJson(deps, "openai", TOKEN_URL3, PROFILES.openai, {
17951
18065
  contentType: "application/x-www-form-urlencoded",
17952
18066
  body: new URLSearchParams({ ...body2, client_id: CLIENT_ID3 }).toString()
17953
18067
  });
@@ -18046,7 +18160,7 @@ var openaiOAuth = {
18046
18160
  if (secrets.accessToken === null)
18047
18161
  return null;
18048
18162
  const accountId = accountIdFromProviderData(providerData);
18049
- const { status, parsed } = await getJson(deps, USAGE_URL3, PROFILES.openai, {
18163
+ const { status, parsed } = await getJson(deps, "openai", USAGE_URL3, PROFILES.openai, {
18050
18164
  accessToken: secrets.accessToken,
18051
18165
  ...accountId === null ? {} : { extraHeaders: [["chatgpt-account-id", accountId]] }
18052
18166
  });
@@ -18064,6 +18178,7 @@ var OAUTH_PROVIDERS = {
18064
18178
  };
18065
18179
  // packages/control/src/oauth/refresh.ts
18066
18180
  function createRefresher(deps) {
18181
+ const logger2 = deps.logger ?? noopLogger;
18067
18182
  const inFlight = new Map;
18068
18183
  async function run(credential) {
18069
18184
  const secrets = await credential.openForRefresh();
@@ -18071,11 +18186,20 @@ function createRefresher(deps) {
18071
18186
  throw new GatewayError("AUTH", `credential ${credential.id} has no refresh token`);
18072
18187
  }
18073
18188
  const provider = deps.providers[credential.provider];
18189
+ logger2.debug("refreshing credential", {
18190
+ provider: credential.provider,
18191
+ credentialId: credential.id
18192
+ });
18074
18193
  let result;
18075
18194
  try {
18076
18195
  result = await provider.refresh(secrets.refreshToken, { http: deps.http, now: deps.now }, credential.providerData);
18077
18196
  } catch (error51) {
18078
18197
  if (error51 instanceof GatewayError && error51.code === "AUTH") {
18198
+ logger2.warn("credential disabled: refresh token rejected", {
18199
+ provider: credential.provider,
18200
+ credentialId: credential.id,
18201
+ code: "AUTH"
18202
+ });
18079
18203
  await deps.store.credentials.update(credential.id, {
18080
18204
  enabled: false,
18081
18205
  disabledReason: "tokenRejected",
@@ -18089,6 +18213,10 @@ function createRefresher(deps) {
18089
18213
  accountEmail: result.accountEmail ?? credential.accountEmail,
18090
18214
  providerData: { ...credential.providerData, ...result.providerData }
18091
18215
  });
18216
+ logger2.debug("credential refreshed", {
18217
+ provider: credential.provider,
18218
+ credentialId: credential.id
18219
+ });
18092
18220
  return result.secrets;
18093
18221
  }
18094
18222
  return function refresh(credential) {
@@ -18696,6 +18824,11 @@ var modelsShow = {
18696
18824
  { header: "WEIGHT", align: "right" },
18697
18825
  { header: "IN $/MTOK", align: "right" },
18698
18826
  { header: "OUT $/MTOK", align: "right" },
18827
+ { header: "CACHE R", align: "right" },
18828
+ { header: "CACHE W 5M", align: "right" },
18829
+ { header: "CACHE W 1H", align: "right" },
18830
+ { header: "CONTEXT", align: "right" },
18831
+ { header: "MAX OUT", align: "right" },
18699
18832
  { header: "CAPABILITIES" }
18700
18833
  ], model.targets.map((target) => [
18701
18834
  provider(ctx, target.provider),
@@ -18704,12 +18837,23 @@ var modelsShow = {
18704
18837
  String(target.weight),
18705
18838
  target.costPerMTok.input.toFixed(2),
18706
18839
  target.costPerMTok.output.toFixed(2),
18840
+ price(target.costPerMTok.cacheRead),
18841
+ price(target.costPerMTok.cacheWrite5m),
18842
+ price(target.costPerMTok.cacheWrite1h),
18843
+ tokens(target.contextWindow),
18844
+ tokens(target.maxOutputTokens),
18707
18845
  capabilityList(target)
18708
18846
  ]))
18709
18847
  ].join(`
18710
18848
  `));
18711
18849
  }
18712
18850
  };
18851
+ function price(value) {
18852
+ return value === undefined ? "\u2014" : value.toFixed(2);
18853
+ }
18854
+ function tokens(value) {
18855
+ return value === undefined ? "\u2014" : value.toLocaleString("en-US");
18856
+ }
18713
18857
  function capabilityList(target) {
18714
18858
  const on = Object.entries(target.capabilities).filter(([, enabled]) => enabled).map(([name]) => name);
18715
18859
  return on.length === 0 ? "\u2014" : on.join(",");
@@ -18741,7 +18885,7 @@ function isCatalogProvider(value) {
18741
18885
  return value in PROVIDER_MODEL_CATALOG;
18742
18886
  }
18743
18887
  function catalogRows() {
18744
- return Object.entries(PROVIDER_MODEL_CATALOG).flatMap(([id, entry]) => entry.models.map((model) => ({ provider: id, id: model.id, label: model.label })));
18888
+ return Object.entries(PROVIDER_MODEL_CATALOG).flatMap(([id, entry]) => entry.models.map((model) => ({ ...model, provider: id })));
18745
18889
  }
18746
18890
  var modelsCatalog = {
18747
18891
  usage: "models catalog [--provider P]",
@@ -18755,15 +18899,29 @@ var modelsCatalog = {
18755
18899
  { header: "MODEL" },
18756
18900
  { header: "LABEL" },
18757
18901
  { header: "IN $/MTOK", align: "right" },
18758
- { header: "OUT $/MTOK", align: "right" }
18902
+ { header: "OUT $/MTOK", align: "right" },
18903
+ { header: "CACHE R", align: "right" },
18904
+ { header: "CACHE W 5M", align: "right" },
18905
+ { header: "CACHE W 1H", align: "right" },
18906
+ { header: "CONTEXT", align: "right" },
18907
+ { header: "CONTEXT OAUTH", align: "right" },
18908
+ { header: "MAX OUT", align: "right" }
18759
18909
  ], entries.map((entry) => {
18760
- const price = catalogPricing(entry.provider, entry.id);
18910
+ const listed = catalogPricing(entry.provider, entry.id);
18911
+ const limits = catalogLimits(entry.provider, entry.id);
18912
+ const oauth = catalogLimits(entry.provider, entry.id, "oauth");
18761
18913
  return [
18762
18914
  provider(ctx, entry.provider),
18763
18915
  entry.id,
18764
18916
  entry.label,
18765
- price === null ? "\u2014" : price.input.toFixed(2),
18766
- price === null ? "\u2014" : price.output.toFixed(2)
18917
+ price(listed?.input),
18918
+ price(listed?.output),
18919
+ price(listed?.cacheRead),
18920
+ price(listed?.cacheWrite5m),
18921
+ price(listed?.cacheWrite1h),
18922
+ tokens(limits?.contextWindow),
18923
+ oauth?.contextWindow === limits?.contextWindow ? "\u2014" : tokens(oauth?.contextWindow),
18924
+ tokens(limits?.maxOutputTokens)
18767
18925
  ];
18768
18926
  })));
18769
18927
  }