omnigateway 0.1.5 → 0.1.7

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 (39) hide show
  1. package/README.md +55 -2
  2. package/bin/omni.js +709 -325
  3. package/gateway.js +1028 -264
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-Ccqsb_Mq.js → Chip-C69u2U_b.js} +2 -2
  6. package/public/assets/{CopyValue-CvEMy9Zo.js → CopyValue-CJy2T0H6.js} +1 -1
  7. package/public/assets/{Field-Devm4K46.js → Field-Sc8mDqJv.js} +1 -1
  8. package/public/assets/{Lamp-BXSUIAtG.js → Lamp-7jD_9929.js} +2 -2
  9. package/public/assets/{Meter-Fn8X7t60.js → Meter-C0nUftAa.js} +2 -2
  10. package/public/assets/{Modal-C4y9zALa.js → Modal-CiVHSJmm.js} +2 -2
  11. package/public/assets/Rack-CMqGBP4I.js +147 -0
  12. package/public/assets/{Readout-BKpOZIGX.js → Readout-Dv2dZWtI.js} +4 -4
  13. package/public/assets/{States-DBzsiHjQ.js → States-D55MhqMx.js} +10 -10
  14. package/public/assets/{Table-DPDJeeVc.js → Table-DBB3jdgi.js} +1 -1
  15. package/public/assets/{Toggle-CBq5XAMU.js → Toggle-BoGjb5oi.js} +3 -3
  16. package/public/assets/_app-p5Mz7sXD.js +1 -0
  17. package/public/assets/_app.accounts-B5wMO9TH.js +51 -0
  18. package/public/assets/_app.console-Bka7AkiL.js +25 -0
  19. package/public/assets/_app.index-MnQuadXb.js +61 -0
  20. package/public/assets/_app.keys-BX8k8COP.js +39 -0
  21. package/public/assets/_app.logs-DD-sd4fn.js +33 -0
  22. package/public/assets/_app.models-CezFNL6y.js +144 -0
  23. package/public/assets/{_app.settings-Du5Nsp9f.js → _app.settings-D1taAt8B.js} +3 -3
  24. package/public/assets/{_app.usage-DKq4Hsia.js → _app.usage-BRhEvwVr.js} +28 -28
  25. package/public/assets/catalog-C_OQ0icG.js +1 -0
  26. package/public/assets/index-vCuK1fRQ.js +170 -0
  27. package/public/assets/{login-Gyf-1fSa.js → login-DaC-wVpI.js} +8 -8
  28. package/public/assets/{queries-BMwc6EvM.js → queries-2E6-CboE.js} +12 -12
  29. package/public/assets/{trash-2-DFVxurNH.js → trash-2--aH2iKg-.js} +1 -1
  30. package/public/index.html +2 -2
  31. package/public/assets/Rack-CRQSwSeB.js +0 -147
  32. package/public/assets/_app-VCodWes4.js +0 -1
  33. package/public/assets/_app.accounts-BP3L84us.js +0 -51
  34. package/public/assets/_app.index-C5Vd-zlp.js +0 -61
  35. package/public/assets/_app.keys-0e1BHrIZ.js +0 -39
  36. package/public/assets/_app.logs-DNSND96R.js +0 -33
  37. package/public/assets/_app.models-DzFIbnem.js +0 -144
  38. package/public/assets/catalog-DVRPlBJJ.js +0 -1
  39. package/public/assets/index-BYyeRTQJ.js +0 -170
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,86 @@ 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 ANSI2 = /\u001B\[[0-9;]*m/g;
192
+ var HEAD = /^(\S+) (DEBUG|INFO|WARN|ERROR) {1,2}(.*)$/;
193
+ var FIELDS = / {2}(?=[a-zA-Z]+=)/;
194
+ function parseLine(raw) {
195
+ const absent = { raw, at: null, level: null, msg: null };
196
+ const match = HEAD.exec(raw.replace(ANSI2, ""));
197
+ if (match === null)
198
+ return absent;
199
+ const [, instant, label, tail] = match;
200
+ if (instant === undefined || label === undefined || tail === undefined)
201
+ return absent;
202
+ const at = Date.parse(instant);
203
+ if (Number.isNaN(at))
204
+ return absent;
205
+ const msg = tail.split(FIELDS, 1)[0] ?? tail;
206
+ return { raw, at, level: label.toLowerCase(), msg };
207
+ }
208
+ var noopLogger = {
209
+ debug: () => {},
210
+ info: () => {},
211
+ warn: () => {},
212
+ error: () => {},
213
+ enabled: () => false
214
+ };
215
+ function parseLogLevel(value) {
216
+ const normalized = value?.trim().toLowerCase();
217
+ if (normalized === undefined || normalized.length === 0)
218
+ return null;
219
+ return Object.hasOwn(LEVELS, normalized) ? normalized : null;
220
+ }
221
+ // packages/ir/src/request.ts
222
+ function cacheControlOf(block) {
223
+ return block.type === "thinking" ? undefined : block.cacheControl;
224
+ }
225
+ // packages/ir/src/stream.ts
226
+ function usageFromPromptTotal(promptTokens, outputTokens, cacheReadTokens, cacheWriteTokens = 0) {
227
+ return {
228
+ inputTokens: Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens),
229
+ outputTokens,
230
+ cacheReadTokens,
231
+ cacheWriteTokens
232
+ };
233
+ }
234
+ // packages/control/src/config.ts
235
+ var MIN_KEY_LENGTH = 16;
236
+ var DECIMAL_INTEGER = /^\d+$/;
237
+ function optionalText(value, fallback) {
238
+ return value?.trim() || fallback;
239
+ }
240
+ function loadConfig(env) {
241
+ const encryptionKey = env.OMNI_ENCRYPTION_KEY;
242
+ if (typeof encryptionKey !== "string" || encryptionKey.length < MIN_KEY_LENGTH) {
243
+ throw new Error(`OMNI_ENCRYPTION_KEY must be set to at least ${MIN_KEY_LENGTH} characters. ` + "Generate one with: openssl rand -base64 32");
244
+ }
245
+ const host = optionalText(env.OMNI_HOST, "127.0.0.1");
246
+ const rawPort = env.OMNI_PORT ?? "9000";
247
+ const port = Number(rawPort);
248
+ if (!DECIMAL_INTEGER.test(rawPort) || !Number.isInteger(port) || port < 1 || port > 65535) {
249
+ throw new Error(`OMNI_PORT must be an integer between 1 and 65535, got "${rawPort}"`);
250
+ }
251
+ const derivedBaseUrl = `http://${host}:${port}`;
252
+ const baseUrl = optionalText(env.OMNI_BASE_URL, derivedBaseUrl).replace(/\/+$/, "") || derivedBaseUrl;
253
+ const staticDir = env.OMNI_STATIC_DIR?.trim();
254
+ const logFile = env.OMNI_LOG_FILE?.trim();
255
+ const rawLogLevel = env.OMNI_LOG_LEVEL?.trim();
256
+ const logLevel = parseLogLevel(rawLogLevel);
257
+ return {
258
+ logLevel: logLevel ?? "info",
259
+ logLevelFallbackFrom: logLevel === null && rawLogLevel ? rawLogLevel : null,
260
+ port,
261
+ host,
262
+ databasePath: optionalText(env.OMNI_DB_PATH, "./omnigateway.db"),
263
+ encryptionKey,
264
+ baseUrl,
265
+ staticDir: staticDir === undefined || staticDir.length === 0 ? null : staticDir,
266
+ logFile: logFile === undefined || logFile.length === 0 ? null : logFile
267
+ };
268
+ }
218
269
  // packages/providers/src/body.ts
219
270
  var BODY_ORDER = {
220
271
  anthropic: [
@@ -301,8 +352,13 @@ function applyAnthropicSystem(system) {
301
352
  let rewritten = text;
302
353
  for (const [from, to] of REWRITES)
303
354
  rewritten = rewritten.replaceAll(from, to);
304
- if (rewritten.trim().length > 0)
305
- kept.push({ type: "text", text: rewritten });
355
+ if (rewritten.trim().length > 0) {
356
+ kept.push({
357
+ type: "text",
358
+ text: rewritten,
359
+ ...block.cache_control === undefined ? {} : { cache_control: block.cache_control }
360
+ });
361
+ }
306
362
  }
307
363
  return [{ type: "text", text: billingBlock() }, { type: "text", text: AGENT_PREAMBLE }, ...kept];
308
364
  }
@@ -606,6 +662,8 @@ async function* decodeAnthropic(messages) {
606
662
  let inputTokens = 0;
607
663
  let cacheReadTokens = 0;
608
664
  let cacheWriteTokens = 0;
665
+ let cacheWrite5mTokens;
666
+ let cacheWrite1hTokens;
609
667
  let outputTokens = 0;
610
668
  let stopReason = "endTurn";
611
669
  let terminal = false;
@@ -619,6 +677,8 @@ async function* decodeAnthropic(messages) {
619
677
  inputTokens = m.usage?.input_tokens ?? 0;
620
678
  cacheReadTokens = m.usage?.cache_read_input_tokens ?? 0;
621
679
  cacheWriteTokens = m.usage?.cache_creation_input_tokens ?? 0;
680
+ cacheWrite5mTokens = m.usage?.cache_creation?.ephemeral_5m_input_tokens;
681
+ cacheWrite1hTokens = m.usage?.cache_creation?.ephemeral_1h_input_tokens;
622
682
  yield { type: "start", id: String(m.id ?? ""), model: String(m.model ?? "") };
623
683
  break;
624
684
  }
@@ -628,7 +688,7 @@ async function* decodeAnthropic(messages) {
628
688
  if (cb.type === "text")
629
689
  yield { type: "blockStart", index, block: { type: "text" } };
630
690
  else if (cb.type === "thinking")
631
- yield { type: "blockStart", index, block: { type: "thinking" } };
691
+ yield { type: "blockStart", index, block: { type: "thinking", signed: true } };
632
692
  else if (cb.type === "tool_use")
633
693
  yield {
634
694
  type: "blockStart",
@@ -648,7 +708,7 @@ async function* decodeAnthropic(messages) {
648
708
  index,
649
709
  delta: { type: "thinking", text: delta.thinking ?? "" }
650
710
  };
651
- else if (delta.type === "signature_delta")
711
+ else if (delta.type === "signature_delta" && (delta.signature ?? "") !== "")
652
712
  yield {
653
713
  type: "blockDelta",
654
714
  index,
@@ -678,7 +738,14 @@ async function* decodeAnthropic(messages) {
678
738
  yield {
679
739
  type: "end",
680
740
  stopReason,
681
- usage: { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens }
741
+ usage: {
742
+ inputTokens,
743
+ outputTokens,
744
+ cacheReadTokens,
745
+ cacheWriteTokens,
746
+ ...cacheWrite5mTokens === undefined ? {} : { cacheWrite5mTokens },
747
+ ...cacheWrite1hTokens === undefined ? {} : { cacheWrite1hTokens }
748
+ }
682
749
  };
683
750
  break;
684
751
  case "error": {
@@ -708,25 +775,36 @@ async function* decodeAnthropic(messages) {
708
775
 
709
776
  // packages/providers/src/anthropic/wire.ts
710
777
  var OAUTH_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
778
+ function wireCacheControl(c) {
779
+ if (c === undefined)
780
+ return {};
781
+ return { cache_control: { type: c.type, ...c.ttl === undefined ? {} : { ttl: c.ttl } } };
782
+ }
783
+ function isReplayableThinking(b) {
784
+ return b.type !== "thinking" || b.signature !== undefined && b.signature !== "";
785
+ }
711
786
  function encodeBlock(b) {
787
+ const cache = wireCacheControl(cacheControlOf(b));
712
788
  switch (b.type) {
713
789
  case "text":
714
- return { type: "text", text: b.text };
790
+ return { type: "text", text: b.text, ...cache };
715
791
  case "image":
716
792
  return {
717
793
  type: "image",
718
- source: { type: "base64", media_type: b.mediaType, data: b.data }
794
+ source: { type: "base64", media_type: b.mediaType, data: b.data },
795
+ ...cache
719
796
  };
720
797
  case "thinking":
721
798
  return { type: "thinking", thinking: b.text, signature: b.signature };
722
799
  case "toolUse":
723
- return { type: "tool_use", id: b.id, name: b.name, input: b.input };
800
+ return { type: "tool_use", id: b.id, name: b.name, input: b.input, ...cache };
724
801
  case "toolResult":
725
802
  return {
726
803
  type: "tool_result",
727
804
  tool_use_id: b.toolUseId,
728
805
  content: b.content,
729
- is_error: b.isError
806
+ is_error: b.isError,
807
+ ...cache
730
808
  };
731
809
  }
732
810
  }
@@ -734,6 +812,16 @@ function encodeSystemTurn(content) {
734
812
  return content.flatMap((b) => b.type === "text" ? [b.text] : []).join(`
735
813
  `);
736
814
  }
815
+ function systemCacheControl(req) {
816
+ const cacheable = req.messages.flatMap((message) => message.content.flatMap((block) => block.type === "thinking" ? [] : [{ role: message.role, block }]));
817
+ const markedSystemBlocks = cacheable.filter(({ role, block }) => role === "system" && cacheControlOf(block) !== undefined);
818
+ const final = markedSystemBlocks.at(-1);
819
+ const promoted = final !== undefined && final.block.type === "text" && cacheable.at(-1) === final ? cacheControlOf(final.block) : undefined;
820
+ return {
821
+ ...promoted === undefined ? {} : { promoted },
822
+ lost: markedSystemBlocks.length > (promoted === undefined ? 0 : 1)
823
+ };
824
+ }
737
825
  function encodeToolChoice(c) {
738
826
  switch (c.type) {
739
827
  case "auto":
@@ -748,16 +836,39 @@ function encodeToolChoice(c) {
748
836
  }
749
837
  function toWire(req, model, opts) {
750
838
  const degradations = [];
751
- let system = req.system?.flatMap((b) => b.type === "text" ? [{ type: "text", text: b.text }] : []);
839
+ const note2 = (d) => {
840
+ if (!degradations.includes(d))
841
+ degradations.push(d);
842
+ };
843
+ let system = req.system?.flatMap((b) => b.type === "text" ? [{ type: "text", text: b.text, ...wireCacheControl(b.cacheControl) }] : []);
752
844
  if (opts.oauth && system?.[0]?.text !== OAUTH_IDENTITY) {
753
845
  system = [{ type: "text", text: OAUTH_IDENTITY }, ...system ?? []];
754
- degradations.push("anthropic:oauth-system-prefix");
846
+ note2("anthropic:oauth-system-prefix");
755
847
  }
848
+ const systemCache = systemCacheControl(req);
849
+ if (systemCache.lost)
850
+ note2("anthropic:system-turn-cache-control-dropped");
756
851
  const body = {
757
852
  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) }),
853
+ messages: req.messages.flatMap((m) => {
854
+ if (m.role !== "system") {
855
+ const replayable = m.content.filter(isReplayableThinking);
856
+ if (replayable.length !== m.content.length)
857
+ note2("anthropic:unsigned-thinking-dropped");
858
+ if (replayable.length === 0)
859
+ return [];
860
+ return [{ role: m.role, content: replayable.map(encodeBlock) }];
861
+ }
862
+ return [{ role: m.role, content: encodeSystemTurn(m.content) }];
863
+ }),
759
864
  max_tokens: req.maxTokens ?? 4096,
760
- stream: req.stream
865
+ stream: req.stream,
866
+ ...systemCache.promoted === undefined ? {} : {
867
+ cache_control: {
868
+ type: systemCache.promoted.type,
869
+ ...systemCache.promoted.ttl === undefined ? {} : { ttl: systemCache.promoted.ttl }
870
+ }
871
+ }
761
872
  };
762
873
  if (system !== undefined && system.length > 0)
763
874
  body.system = system;
@@ -769,7 +880,8 @@ function toWire(req, model, opts) {
769
880
  body.tools = req.tools.map((t) => ({
770
881
  name: t.name,
771
882
  ...t.description === undefined ? {} : { description: t.description },
772
- input_schema: t.inputSchema
883
+ input_schema: t.inputSchema,
884
+ ...wireCacheControl(t.cacheControl)
773
885
  }));
774
886
  }
775
887
  if (req.toolChoice !== undefined)
@@ -809,12 +921,13 @@ var anthropicAdapter = {
809
921
  const { body, degradations } = toWire(req.request, req.model, { oauth });
810
922
  const withSystem = {
811
923
  ...body,
812
- system: applyAnthropicSystem(body.system ?? [])
924
+ system: applyAnthropicSystem(body.system ?? []),
925
+ stream: true
813
926
  };
814
927
  const protocol = [
815
928
  ["Content-Type", "application/json"],
816
929
  ["anthropic-version", API_VERSION],
817
- ["Accept", req.request.stream ? "text/event-stream" : "application/json"]
930
+ ["Accept", "text/event-stream"]
818
931
  ];
819
932
  const betas = new Set(req.request.betas ?? []);
820
933
  if (oauth) {
@@ -833,6 +946,7 @@ var anthropicAdapter = {
833
946
  const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
834
947
  const bodyString = signAnthropicBody(JSON.stringify(orderFields(withSystem, BODY_ORDER.anthropic)));
835
948
  const res = await req.http({
949
+ provider: "anthropic",
836
950
  url: BASE_URL,
837
951
  method: "POST",
838
952
  headers,
@@ -853,22 +967,26 @@ var ANTHROPIC_MODELS = {
853
967
  {
854
968
  id: "claude-fable-5",
855
969
  label: "Claude Fable 5",
856
- pricing: { input: 10, output: 50, cacheRead: 1 }
970
+ pricing: { input: 10, output: 50, cacheRead: 1, cacheWrite5m: 12.5, cacheWrite1h: 20 },
971
+ limits: { contextWindow: 1e6, maxOutputTokens: 128000 }
857
972
  },
858
973
  {
859
974
  id: "claude-opus-5",
860
975
  label: "Claude Opus 5",
861
- pricing: { input: 5, output: 25, cacheRead: 0.5 }
976
+ pricing: { input: 5, output: 25, cacheRead: 0.5, cacheWrite5m: 6.25, cacheWrite1h: 10 },
977
+ limits: { contextWindow: 1e6, maxOutputTokens: 128000 }
862
978
  },
863
979
  {
864
980
  id: "claude-sonnet-5",
865
981
  label: "Claude Sonnet 5",
866
- pricing: { input: 3, output: 15, cacheRead: 0.3 }
982
+ pricing: { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 3.75, cacheWrite1h: 6 },
983
+ limits: { contextWindow: 1e6, maxOutputTokens: 128000 }
867
984
  },
868
985
  {
869
986
  id: "claude-haiku-4-5",
870
987
  label: "Claude Haiku 4.5",
871
- pricing: { input: 1, output: 5, cacheRead: 0.1 }
988
+ pricing: { input: 1, output: 5, cacheRead: 0.1, cacheWrite5m: 1.25, cacheWrite1h: 2 },
989
+ limits: { contextWindow: 200000, maxOutputTokens: 64000 }
872
990
  }
873
991
  ]
874
992
  };
@@ -880,22 +998,26 @@ var KIMI_MODELS = {
880
998
  {
881
999
  id: "k3-256k",
882
1000
  label: "Kimi K3 \u2014 256K",
883
- pricing: { input: 3, output: 15, cacheRead: 0.3 }
1001
+ pricing: { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 0, cacheWrite1h: 0 },
1002
+ limits: { contextWindow: 262144, maxOutputTokens: 131072 }
884
1003
  },
885
1004
  {
886
1005
  id: "k3",
887
1006
  label: "Kimi K3 \u2014 up to 1M",
888
- pricing: { input: 3, output: 15, cacheRead: 0.3 }
1007
+ pricing: { input: 3, output: 15, cacheRead: 0.3, cacheWrite5m: 0, cacheWrite1h: 0 },
1008
+ limits: { contextWindow: 1048576, maxOutputTokens: 131072 }
889
1009
  },
890
1010
  {
891
1011
  id: "kimi-for-coding",
892
1012
  label: "Kimi K2.7 Code",
893
- pricing: { input: 0.95, output: 4, cacheRead: 0.19 }
1013
+ pricing: { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite5m: 0, cacheWrite1h: 0 },
1014
+ limits: { contextWindow: 262144, maxOutputTokens: 131072 }
894
1015
  },
895
1016
  {
896
1017
  id: "kimi-for-coding-highspeed",
897
1018
  label: "Kimi K2.7 Code \u2014 High Speed",
898
- pricing: { input: 0.95, output: 8, cacheRead: 0.19 }
1019
+ pricing: { input: 0.95, output: 8, cacheRead: 0.19, cacheWrite5m: 0, cacheWrite1h: 0 },
1020
+ limits: { contextWindow: 262144, maxOutputTokens: 131072 }
899
1021
  }
900
1022
  ]
901
1023
  };
@@ -907,22 +1029,30 @@ var OPENAI_MODELS = {
907
1029
  {
908
1030
  id: "gpt-5.6",
909
1031
  label: "GPT-5.6 \u2014 routes to Sol",
910
- pricing: { input: 5, output: 30, cacheRead: 0.5 }
1032
+ pricing: { input: 5, output: 30, cacheRead: 0.5, cacheWrite5m: 0, cacheWrite1h: 0 },
1033
+ limits: { contextWindow: 922000, maxOutputTokens: 128000 },
1034
+ oauthLimits: { contextWindow: 272000, maxOutputTokens: 128000 }
911
1035
  },
912
1036
  {
913
1037
  id: "gpt-5.6-sol",
914
1038
  label: "GPT-5.6 Sol \u2014 deepest reasoning",
915
- pricing: { input: 5, output: 30, cacheRead: 0.5 }
1039
+ pricing: { input: 5, output: 30, cacheRead: 0.5, cacheWrite5m: 0, cacheWrite1h: 0 },
1040
+ limits: { contextWindow: 922000, maxOutputTokens: 128000 },
1041
+ oauthLimits: { contextWindow: 272000, maxOutputTokens: 128000 }
916
1042
  },
917
1043
  {
918
1044
  id: "gpt-5.6-terra",
919
1045
  label: "GPT-5.6 Terra \u2014 balanced",
920
- pricing: { input: 2, output: 12, cacheRead: 0.2 }
1046
+ pricing: { input: 2, output: 12, cacheRead: 0.2, cacheWrite5m: 0, cacheWrite1h: 0 },
1047
+ limits: { contextWindow: 922000, maxOutputTokens: 128000 },
1048
+ oauthLimits: { contextWindow: 272000, maxOutputTokens: 128000 }
921
1049
  },
922
1050
  {
923
1051
  id: "gpt-5.6-luna",
924
1052
  label: "GPT-5.6 Luna \u2014 fastest",
925
- pricing: { input: 0.2, output: 1.2, cacheRead: 0.02 }
1053
+ pricing: { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite5m: 0, cacheWrite1h: 0 },
1054
+ limits: { contextWindow: 922000, maxOutputTokens: 128000 },
1055
+ oauthLimits: { contextWindow: 272000, maxOutputTokens: 128000 }
926
1056
  }
927
1057
  ]
928
1058
  };
@@ -934,15 +1064,38 @@ var PROVIDER_MODEL_CATALOG = {
934
1064
  kimi: KIMI_MODELS
935
1065
  };
936
1066
  function catalogPricing(provider, model) {
937
- return PROVIDER_MODEL_CATALOG[provider].models.find((entry) => entry.id === model)?.pricing ?? null;
1067
+ return PROVIDER_MODEL_CATALOG[provider]?.models.find((entry) => entry.id === model)?.pricing ?? null;
1068
+ }
1069
+ function catalogLimits(provider, model, auth = "apiKey") {
1070
+ const entry = PROVIDER_MODEL_CATALOG[provider]?.models.find((choice) => choice.id === model);
1071
+ if (entry === undefined)
1072
+ return null;
1073
+ return (auth === "oauth" ? entry.oauthLimits : undefined) ?? entry.limits;
938
1074
  }
939
1075
  // packages/providers/src/http-client.ts
940
1076
  import { request as httpRequest } from "http";
941
1077
  import { request as httpsRequest } from "https";
942
1078
  import { Readable } from "stream";
943
- function nodeHttpClient() {
1079
+ function nodeHttpClient(options = {}) {
1080
+ const logger2 = options.logger ?? noopLogger;
1081
+ const now = options.now ?? (() => Date.now());
944
1082
  return (req) => new Promise((resolve, reject) => {
945
1083
  const url = new URL(req.url);
1084
+ const startedAt = now();
1085
+ let traced = false;
1086
+ const trace = (status, failed = false) => {
1087
+ if (traced || !logger2.enabled("debug"))
1088
+ return;
1089
+ traced = true;
1090
+ logger2.debug("upstream http", {
1091
+ provider: req.provider,
1092
+ status,
1093
+ host: url.host,
1094
+ path: url.pathname,
1095
+ durationMs: now() - startedAt,
1096
+ reason: failed ? "transport error" : undefined
1097
+ });
1098
+ };
946
1099
  const send = url.protocol === "https:" ? httpsRequest : httpRequest;
947
1100
  const bodyBytes = Buffer.from(req.body, "utf8");
948
1101
  const headers = {};
@@ -970,6 +1123,7 @@ function nodeHttpClient() {
970
1123
  else if (typeof v === "string")
971
1124
  responseHeaders.set(k, v);
972
1125
  }
1126
+ trace(incoming.statusCode);
973
1127
  resolve({
974
1128
  status: incoming.statusCode ?? 0,
975
1129
  headers: responseHeaders,
@@ -985,17 +1139,17 @@ function nodeHttpClient() {
985
1139
  });
986
1140
  });
987
1141
  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
1142
  outgoing.on("error", (err) => {
995
1143
  req.signal.removeEventListener("abort", onAbort);
1144
+ trace(undefined, true);
996
1145
  reject(err);
997
1146
  });
998
1147
  outgoing.on("close", () => req.signal.removeEventListener("abort", onAbort));
1148
+ if (req.signal.aborted) {
1149
+ outgoing.destroy(new Error("aborted"));
1150
+ return;
1151
+ }
1152
+ req.signal.addEventListener("abort", onAbort, { once: true });
999
1153
  if (bodyBytes.byteLength > 0)
1000
1154
  outgoing.write(bodyBytes);
1001
1155
  outgoing.end();
@@ -1064,12 +1218,8 @@ async function* decodeChat(messages) {
1064
1218
  yield { type: "start", id: String(d.id ?? ""), model: String(d.model ?? "") };
1065
1219
  }
1066
1220
  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
- };
1221
+ const details = d.usage.prompt_tokens_details;
1222
+ 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
1223
  }
1074
1224
  const choice = d.choices?.[0];
1075
1225
  if (!choice)
@@ -1195,7 +1345,12 @@ function toChatWire(req, model) {
1195
1345
  `) });
1196
1346
  }
1197
1347
  }
1198
- const body = { model, messages, stream: req.stream };
1348
+ const body = {
1349
+ model,
1350
+ messages,
1351
+ stream: req.stream,
1352
+ stream_options: { include_usage: true }
1353
+ };
1199
1354
  if (req.maxTokens !== undefined)
1200
1355
  body.max_tokens = req.maxTokens;
1201
1356
  if (req.temperature !== undefined)
@@ -1236,6 +1391,7 @@ var kimiAdapter = {
1236
1391
  const profile = PROFILES.kimi;
1237
1392
  const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
1238
1393
  const res = await req.http({
1394
+ provider: "kimi",
1239
1395
  url: BASE_URL2,
1240
1396
  method: "POST",
1241
1397
  headers,
@@ -1366,12 +1522,7 @@ async function* decodeResponses(messages) {
1366
1522
  yield {
1367
1523
  type: "end",
1368
1524
  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
- }
1525
+ 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
1526
  };
1376
1527
  break;
1377
1528
  }
@@ -1544,6 +1695,7 @@ var openaiAdapter = {
1544
1695
  const profile = PROFILES.openai;
1545
1696
  const headers = orderHeaders(mergeHeaders(profile.headers, protocol), profile.order);
1546
1697
  const res = await req.http({
1698
+ provider: "openai",
1547
1699
  url: oauth ? OAUTH_URL : API_URL,
1548
1700
  method: "POST",
1549
1701
  headers,
@@ -1558,13 +1710,14 @@ var openaiAdapter = {
1558
1710
  }
1559
1711
  };
1560
1712
  // packages/control/src/oauth/types.ts
1561
- async function postJson(deps, url, profile2, opts) {
1713
+ async function postJson(deps, provider, url, profile2, opts) {
1562
1714
  const headers = orderHeaders(mergeHeaders(profile2.headers, [
1563
1715
  ["Content-Type", opts.contentType],
1564
1716
  ["Accept", "application/json"],
1565
1717
  ...opts.extraHeaders ?? []
1566
1718
  ]), profile2.order);
1567
1719
  const res = await deps.http({
1720
+ provider,
1568
1721
  url,
1569
1722
  method: "POST",
1570
1723
  headers,
@@ -1578,13 +1731,14 @@ async function postJson(deps, url, profile2, opts) {
1578
1731
  } catch {}
1579
1732
  return { status: res.status, parsed };
1580
1733
  }
1581
- async function getJson(deps, url, profile2, opts) {
1734
+ async function getJson(deps, provider, url, profile2, opts) {
1582
1735
  const headers = orderHeaders(mergeHeaders(profile2.headers, [
1583
1736
  ["Authorization", `Bearer ${opts.accessToken}`],
1584
1737
  ["Accept", "application/json"],
1585
1738
  ...opts.extraHeaders ?? []
1586
1739
  ]), profile2.order);
1587
1740
  const res = await deps.http({
1741
+ provider,
1588
1742
  url,
1589
1743
  method: "GET",
1590
1744
  headers,
@@ -1778,7 +1932,7 @@ function deviceCodeFrom(value) {
1778
1932
  };
1779
1933
  }
1780
1934
  async function post(url, body2, device, deps) {
1781
- const { status, parsed } = await postJson(deps, url, PROFILES.kimi, {
1935
+ const { status, parsed } = await postJson(deps, "kimi", url, PROFILES.kimi, {
1782
1936
  contentType: "application/x-www-form-urlencoded",
1783
1937
  body: new URLSearchParams({ ...body2, client_id: CLIENT_ID }).toString(),
1784
1938
  extraHeaders: kimiDeviceHeaders(device)
@@ -1881,7 +2035,7 @@ var kimiOAuth = {
1881
2035
  if (secrets.accessToken === null)
1882
2036
  return null;
1883
2037
  const device = deviceFrom(providerData);
1884
- const { status, parsed } = await getJson(deps, USAGE_URL, PROFILES.kimi, {
2038
+ const { status, parsed } = await getJson(deps, "kimi", USAGE_URL, PROFILES.kimi, {
1885
2039
  accessToken: secrets.accessToken,
1886
2040
  extraHeaders: kimiDeviceHeaders(device)
1887
2041
  });
@@ -1950,6 +2104,7 @@ function deviceIdFrom(start) {
1950
2104
  return deviceId;
1951
2105
  }
1952
2106
  function createConnectFlows(deps) {
2107
+ const logger2 = deps.logger ?? noopLogger;
1953
2108
  const flows = createPendingFlows({ now: deps.now, ttlMs: FLOW_TTL_MS });
1954
2109
  const pollsInFlight = new Map;
1955
2110
  const callbackUri = (provider) => provider === "openai" ? "http://localhost:1455/auth/callback" : "";
@@ -1994,6 +2149,10 @@ function createConnectFlows(deps) {
1994
2149
  disabledAt: null,
1995
2150
  ...result.secrets
1996
2151
  });
2152
+ logger2.info("oauth connect completed", {
2153
+ provider: flow.provider,
2154
+ credentialId: id
2155
+ });
1997
2156
  return { id };
1998
2157
  }
1999
2158
  async function pollOutcome(flowId, poll) {
@@ -2066,6 +2225,62 @@ function createConnectFlows(deps) {
2066
2225
  }
2067
2226
  };
2068
2227
  }
2228
+ // packages/control/src/console.ts
2229
+ var UNIT_NAME = "omnigateway.service";
2230
+ var MAX_CONSOLE_LINES = 500;
2231
+ var DEFAULT_CONSOLE_LINES = 200;
2232
+ function resolveConsoleSource(input) {
2233
+ const path = input.logFile?.trim();
2234
+ if (path !== undefined && path.length > 0)
2235
+ return { kind: "file", path };
2236
+ if (input.unitInstalled)
2237
+ return { kind: "journal", unit: UNIT_NAME, scope: input.scope };
2238
+ return { kind: "none" };
2239
+ }
2240
+ function consoleLimit(requested) {
2241
+ const value = requested === undefined ? DEFAULT_CONSOLE_LINES : Number(requested);
2242
+ if (!Number.isFinite(value))
2243
+ return DEFAULT_CONSOLE_LINES;
2244
+ return Math.floor(Math.min(Math.max(1, value), MAX_CONSOLE_LINES));
2245
+ }
2246
+ function keep(line, query) {
2247
+ if (query.since !== undefined && (line.at === null || line.at <= query.since))
2248
+ return false;
2249
+ if (query.level === undefined || line.level === null)
2250
+ return true;
2251
+ return LEVELS[line.level] >= LEVELS[query.level];
2252
+ }
2253
+ var SCAN_FACTOR = 20;
2254
+ var SCAN_LIMIT = 5000;
2255
+ function scanWidth(query) {
2256
+ const filtered = query.level !== undefined || query.since !== undefined;
2257
+ return filtered ? Math.min(query.lines * SCAN_FACTOR, SCAN_LIMIT) : query.lines;
2258
+ }
2259
+ async function readSource(deps, source, lines) {
2260
+ if (source.kind === "none")
2261
+ return "";
2262
+ if (source.kind === "file")
2263
+ return deps.readFile(source.path, lines) ?? "";
2264
+ const scope = source.scope === "system" ? [] : ["--user"];
2265
+ const result = await deps.run([
2266
+ "journalctl",
2267
+ ...scope,
2268
+ "-u",
2269
+ source.unit,
2270
+ "-n",
2271
+ String(lines),
2272
+ "--no-pager",
2273
+ "--output=cat"
2274
+ ]);
2275
+ return result.code === 0 ? result.stdout : "";
2276
+ }
2277
+ async function readConsole(deps, source, query) {
2278
+ const limited = { ...query, lines: consoleLimit(query.lines) };
2279
+ const text = await readSource(deps, source, scanWidth(limited));
2280
+ const lines = text.split(`
2281
+ `).filter((raw) => raw.trim().length > 0).map(parseLine).filter((line) => keep(line, limited)).slice(-limited.lines);
2282
+ return source.kind === "file" ? { source: "file", path: source.path, lines } : { source: source.kind, lines };
2283
+ }
2069
2284
  // node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
2070
2285
  var exports_external = {};
2071
2286
  __export(exports_external, {
@@ -16369,28 +16584,18 @@ var modelSchema = exports_external.object({
16369
16584
  costPerMTok: exports_external.object({
16370
16585
  input: exports_external.number().min(0),
16371
16586
  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)
16587
+ cacheRead: exports_external.number().min(0).optional(),
16588
+ cacheWrite5m: exports_external.number().min(0).optional(),
16589
+ cacheWrite1h: exports_external.number().min(0).optional()
16387
16590
  }),
16591
+ contextWindow: exports_external.number().int().positive().optional(),
16592
+ maxOutputTokens: exports_external.number().int().positive().optional(),
16388
16593
  capabilities: exports_external.object({
16389
16594
  tools: exports_external.boolean(),
16390
16595
  images: exports_external.boolean(),
16391
16596
  reasoning: exports_external.boolean()
16392
16597
  })
16393
- }))).min(1, "a virtual model needs at least one target")
16598
+ })).min(1, "a virtual model needs at least one target")
16394
16599
  });
16395
16600
  var keyCreateSchema = exports_external.object({
16396
16601
  label: exports_external.string().min(1).default("api key"),
@@ -16468,7 +16673,7 @@ async function getCredential(store, id) {
16468
16673
  throw new GatewayError("BAD_REQUEST", "no such credential");
16469
16674
  return summarizeCredential(credential);
16470
16675
  }
16471
- async function createApiKeyCredential(store, input) {
16676
+ async function createApiKeyCredential(store, input, logger2 = noopLogger) {
16472
16677
  const provider = parseOrThrow(providerIdSchema, input.provider);
16473
16678
  if (typeof input.apiKey !== "string" || input.apiKey.trim().length === 0) {
16474
16679
  throw new GatewayError("BAD_REQUEST", "apiKey: must not be empty");
@@ -16495,6 +16700,7 @@ async function createApiKeyCredential(store, input) {
16495
16700
  apiKey: input.apiKey,
16496
16701
  idToken: null
16497
16702
  });
16703
+ logger2.info("credential added", { credentialId: created.id, provider: created.provider });
16498
16704
  return summarizeCredential(created);
16499
16705
  }
16500
16706
  async function refreshCredential(deps, id) {
@@ -16554,6 +16760,12 @@ async function patchCredential(deps, id, input) {
16554
16760
  ...patch.tier === undefined ? {} : { tier: patch.tier },
16555
16761
  ...patch.weight === undefined ? {} : { weight: patch.weight }
16556
16762
  });
16763
+ if (patch.enabled !== undefined && patch.enabled !== existing.enabled) {
16764
+ (deps.logger ?? noopLogger).info(patch.enabled ? "credential enabled" : "credential disabled", {
16765
+ credentialId: id,
16766
+ provider: existing.provider
16767
+ });
16768
+ }
16557
16769
  }
16558
16770
  async function removeCredential(store, id) {
16559
16771
  await store.credentials.remove(id);
@@ -17718,7 +17930,9 @@ function createUsageRepo(db) {
17718
17930
 
17719
17931
  // packages/store/src/sqlite/store.ts
17720
17932
  async function createStore(opts) {
17933
+ const logger2 = opts.logger ?? noopLogger;
17721
17934
  const db = openDb(opts.path);
17935
+ logger2.debug("store opened", { path: opts.path });
17722
17936
  const listeners = new Set;
17723
17937
  const emit2 = (change) => {
17724
17938
  for (const listener of listeners) {
@@ -17811,7 +18025,7 @@ var REDIRECT_URI = "https://platform.claude.com/oauth/code/callback";
17811
18025
  var SCOPES = "org:create_api_key user:profile user:inference";
17812
18026
  var USAGE_URL2 = "https://api.anthropic.com/api/oauth/usage";
17813
18027
  async function postToken(body2, deps) {
17814
- const { status, parsed } = await postJson(deps, TOKEN_URL2, PROFILES.anthropic, {
18028
+ const { status, parsed } = await postJson(deps, "anthropic", TOKEN_URL2, PROFILES.anthropic, {
17815
18029
  contentType: "application/json",
17816
18030
  body: JSON.stringify({ ...body2, client_id: CLIENT_ID2 })
17817
18031
  });
@@ -17889,7 +18103,7 @@ var anthropicOAuth = {
17889
18103
  async usage(secrets, deps) {
17890
18104
  if (secrets.accessToken === null)
17891
18105
  return null;
17892
- const { status, parsed } = await getJson(deps, USAGE_URL2, PROFILES.anthropic, {
18106
+ const { status, parsed } = await getJson(deps, "anthropic", USAGE_URL2, PROFILES.anthropic, {
17893
18107
  accessToken: secrets.accessToken,
17894
18108
  extraHeaders: [
17895
18109
  ["anthropic-beta", "oauth-2025-04-20"],
@@ -17947,7 +18161,7 @@ function parseTokenResponse(value) {
17947
18161
  };
17948
18162
  }
17949
18163
  async function postToken2(body2, deps) {
17950
- const { status, parsed } = await postJson(deps, TOKEN_URL3, PROFILES.openai, {
18164
+ const { status, parsed } = await postJson(deps, "openai", TOKEN_URL3, PROFILES.openai, {
17951
18165
  contentType: "application/x-www-form-urlencoded",
17952
18166
  body: new URLSearchParams({ ...body2, client_id: CLIENT_ID3 }).toString()
17953
18167
  });
@@ -18046,7 +18260,7 @@ var openaiOAuth = {
18046
18260
  if (secrets.accessToken === null)
18047
18261
  return null;
18048
18262
  const accountId = accountIdFromProviderData(providerData);
18049
- const { status, parsed } = await getJson(deps, USAGE_URL3, PROFILES.openai, {
18263
+ const { status, parsed } = await getJson(deps, "openai", USAGE_URL3, PROFILES.openai, {
18050
18264
  accessToken: secrets.accessToken,
18051
18265
  ...accountId === null ? {} : { extraHeaders: [["chatgpt-account-id", accountId]] }
18052
18266
  });
@@ -18064,6 +18278,7 @@ var OAUTH_PROVIDERS = {
18064
18278
  };
18065
18279
  // packages/control/src/oauth/refresh.ts
18066
18280
  function createRefresher(deps) {
18281
+ const logger2 = deps.logger ?? noopLogger;
18067
18282
  const inFlight = new Map;
18068
18283
  async function run(credential) {
18069
18284
  const secrets = await credential.openForRefresh();
@@ -18071,11 +18286,20 @@ function createRefresher(deps) {
18071
18286
  throw new GatewayError("AUTH", `credential ${credential.id} has no refresh token`);
18072
18287
  }
18073
18288
  const provider = deps.providers[credential.provider];
18289
+ logger2.debug("refreshing credential", {
18290
+ provider: credential.provider,
18291
+ credentialId: credential.id
18292
+ });
18074
18293
  let result;
18075
18294
  try {
18076
18295
  result = await provider.refresh(secrets.refreshToken, { http: deps.http, now: deps.now }, credential.providerData);
18077
18296
  } catch (error51) {
18078
18297
  if (error51 instanceof GatewayError && error51.code === "AUTH") {
18298
+ logger2.warn("credential disabled: refresh token rejected", {
18299
+ provider: credential.provider,
18300
+ credentialId: credential.id,
18301
+ code: "AUTH"
18302
+ });
18079
18303
  await deps.store.credentials.update(credential.id, {
18080
18304
  enabled: false,
18081
18305
  disabledReason: "tokenRejected",
@@ -18089,6 +18313,10 @@ function createRefresher(deps) {
18089
18313
  accountEmail: result.accountEmail ?? credential.accountEmail,
18090
18314
  providerData: { ...credential.providerData, ...result.providerData }
18091
18315
  });
18316
+ logger2.debug("credential refreshed", {
18317
+ provider: credential.provider,
18318
+ credentialId: credential.id
18319
+ });
18092
18320
  return result.secrets;
18093
18321
  }
18094
18322
  return function refresh(credential) {
@@ -18113,6 +18341,50 @@ async function putSettings(store, input) {
18113
18341
  await store.config.putSettings(settings);
18114
18342
  return settings;
18115
18343
  }
18344
+ // packages/control/src/tail.ts
18345
+ import { closeSync, fstatSync, openSync, readSync, statSync } from "fs";
18346
+ var CHUNK = 64 * 1024;
18347
+ var MAX_BYTES = 8 * 1024 * 1024;
18348
+ function tailFile(path, lines) {
18349
+ let fd;
18350
+ try {
18351
+ fd = openSync(path, "r");
18352
+ } catch {
18353
+ return null;
18354
+ }
18355
+ try {
18356
+ const size = fstatSync(fd).size;
18357
+ if (size === 0)
18358
+ return "";
18359
+ const budget = Math.min(size, MAX_BYTES);
18360
+ let read = 0;
18361
+ let newlines = 0;
18362
+ const chunks = [];
18363
+ while (read < budget && newlines <= lines) {
18364
+ const span = Math.min(CHUNK, budget - read);
18365
+ const buffer = Buffer.allocUnsafe(span);
18366
+ const position = size - read - span;
18367
+ readSync(fd, buffer, 0, span, position);
18368
+ chunks.unshift(buffer);
18369
+ read += span;
18370
+ for (const byte of buffer)
18371
+ if (byte === 10)
18372
+ newlines++;
18373
+ }
18374
+ return Buffer.concat(chunks).toString("utf8");
18375
+ } catch {
18376
+ return null;
18377
+ } finally {
18378
+ closeSync(fd);
18379
+ }
18380
+ }
18381
+ function fileExists(path) {
18382
+ try {
18383
+ return statSync(path).isFile();
18384
+ } catch {
18385
+ return false;
18386
+ }
18387
+ }
18116
18388
  // packages/control/src/usage.ts
18117
18389
  var MAX_LOG_LIMIT = 500;
18118
18390
  var DEFAULT_LOG_LIMIT = 100;
@@ -18347,6 +18619,278 @@ async function pollUntilAuthorized(flows, start, deps) {
18347
18619
  }
18348
18620
  }
18349
18621
 
18622
+ // apps/cli/src/service.ts
18623
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
18624
+ import { homedir as homedir2 } from "os";
18625
+ import { dirname, join as join2 } from "path";
18626
+ var UNIT_NAME2 = "omnigateway.service";
18627
+ function unitPath(scope) {
18628
+ return scope === "system" ? join2("/etc/systemd/system", UNIT_NAME2) : join2(homedir2(), ".config", "systemd", "user", UNIT_NAME2);
18629
+ }
18630
+ function systemctlArgs(scope, args) {
18631
+ return scope === "system" ? ["systemctl", ...args] : ["systemctl", "--user", ...args];
18632
+ }
18633
+ function pidFile(stateDir) {
18634
+ return join2(stateDir, "gateway.pid");
18635
+ }
18636
+ function logFile(stateDir) {
18637
+ return join2(stateDir, "gateway.log");
18638
+ }
18639
+ function supervisedLogFile(deps) {
18640
+ const configured = deps.logFile?.trim();
18641
+ return configured !== undefined && configured.length > 0 ? configured : logFile(deps.stateDir);
18642
+ }
18643
+ function defaultStateDir(env2) {
18644
+ const xdg = env2.XDG_STATE_HOME;
18645
+ const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join2(homedir2(), ".local", "state");
18646
+ return join2(base, "omnigateway");
18647
+ }
18648
+ function unitFile(input) {
18649
+ return `[Unit]
18650
+ Description=${input.description ?? "OmniGateway"}
18651
+ After=network-online.target
18652
+ Wants=network-online.target
18653
+
18654
+ [Service]
18655
+ Type=simple
18656
+ WorkingDirectory=${input.root}
18657
+ EnvironmentFile=${join2(input.root, ".env")}
18658
+ ExecStart=${input.bun} ${input.entrypoint}
18659
+ Restart=on-failure
18660
+ RestartSec=2
18661
+
18662
+ [Install]
18663
+ WantedBy=default.target
18664
+ `;
18665
+ }
18666
+ function unitInstalled(deps) {
18667
+ return existsSync2(deps.unitPath);
18668
+ }
18669
+ function readPid(stateDir) {
18670
+ const file2 = pidFile(stateDir);
18671
+ if (!existsSync2(file2))
18672
+ return null;
18673
+ const pid = Number(readFileSync2(file2, "utf8").trim());
18674
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
18675
+ }
18676
+ function livePid(deps) {
18677
+ const pid = readPid(deps.stateDir);
18678
+ if (pid === null)
18679
+ return null;
18680
+ if (deps.alive(pid))
18681
+ return pid;
18682
+ rmSync(pidFile(deps.stateDir), { force: true });
18683
+ return null;
18684
+ }
18685
+ async function status(deps) {
18686
+ if (unitInstalled(deps)) {
18687
+ const active = await deps.run(systemctlArgs(deps.scope, ["is-active", UNIT_NAME2]));
18688
+ const state = active.stdout.trim() || active.stderr.trim() || "unknown";
18689
+ const shown = await deps.run(systemctlArgs(deps.scope, ["show", UNIT_NAME2, "--property=MainPID", "--value"]));
18690
+ const pid2 = Number(shown.stdout.trim());
18691
+ return {
18692
+ supervisor: "systemd",
18693
+ running: state === "active",
18694
+ pid: Number.isInteger(pid2) && pid2 > 0 ? pid2 : null,
18695
+ state,
18696
+ unitPath: deps.unitPath,
18697
+ logFile: null
18698
+ };
18699
+ }
18700
+ const pid = livePid(deps);
18701
+ return {
18702
+ supervisor: pid === null ? "none" : "pidfile",
18703
+ running: pid !== null,
18704
+ pid,
18705
+ state: null,
18706
+ unitPath: null,
18707
+ logFile: supervisedLogFile(deps)
18708
+ };
18709
+ }
18710
+ async function start(deps, input) {
18711
+ if (unitInstalled(deps)) {
18712
+ const result = await deps.run(systemctlArgs(deps.scope, ["start", UNIT_NAME2]));
18713
+ if (result.code !== 0) {
18714
+ throw new Error(result.stderr.trim() || `systemctl start exited ${result.code}`);
18715
+ }
18716
+ return {
18717
+ supervisor: "systemd",
18718
+ pid: null,
18719
+ healthy: await waitForHealth(deps, input.baseUrl)
18720
+ };
18721
+ }
18722
+ const running = livePid(deps);
18723
+ if (running !== null) {
18724
+ return {
18725
+ supervisor: "pidfile",
18726
+ pid: running,
18727
+ healthy: await waitForHealth(deps, input.baseUrl)
18728
+ };
18729
+ }
18730
+ mkdirSync(deps.stateDir, { recursive: true });
18731
+ const file2 = supervisedLogFile(deps);
18732
+ mkdirSync(dirname(file2), { recursive: true });
18733
+ const pid = deps.spawn({
18734
+ argv: input.argv,
18735
+ cwd: deps.root,
18736
+ env: { ...input.env, OMNI_LOG_FILE: file2 },
18737
+ logFile: file2
18738
+ });
18739
+ writeFileSync(pidFile(deps.stateDir), `${pid}
18740
+ `);
18741
+ return { supervisor: "pidfile", pid, healthy: await waitForHealth(deps, input.baseUrl) };
18742
+ }
18743
+ var HEALTH_TIMEOUT_MS = 1e4;
18744
+ var HEALTH_INTERVAL_MS = 200;
18745
+ async function waitForHealth(deps, baseUrl) {
18746
+ const attempts = Math.ceil(HEALTH_TIMEOUT_MS / HEALTH_INTERVAL_MS);
18747
+ for (let attempt = 0;attempt < attempts; attempt++) {
18748
+ if (await deps.probe(baseUrl))
18749
+ return true;
18750
+ await deps.sleep(HEALTH_INTERVAL_MS);
18751
+ }
18752
+ return false;
18753
+ }
18754
+ var STOP_GRACE_MS = 5000;
18755
+ var STOP_INTERVAL_MS = 100;
18756
+ async function stop(deps) {
18757
+ if (unitInstalled(deps)) {
18758
+ const result = await deps.run(systemctlArgs(deps.scope, ["stop", UNIT_NAME2]));
18759
+ if (result.code !== 0) {
18760
+ throw new Error(result.stderr.trim() || `systemctl stop exited ${result.code}`);
18761
+ }
18762
+ return { supervisor: "systemd", stopped: true };
18763
+ }
18764
+ const pid = livePid(deps);
18765
+ if (pid === null)
18766
+ return { supervisor: "none", stopped: false };
18767
+ deps.kill(pid, "SIGTERM");
18768
+ const attempts = Math.ceil(STOP_GRACE_MS / STOP_INTERVAL_MS);
18769
+ for (let attempt = 0;deps.alive(pid); attempt++) {
18770
+ if (attempt >= attempts) {
18771
+ deps.kill(pid, "SIGKILL");
18772
+ break;
18773
+ }
18774
+ await deps.sleep(STOP_INTERVAL_MS);
18775
+ }
18776
+ rmSync(pidFile(deps.stateDir), { force: true });
18777
+ return { supervisor: "pidfile", stopped: true };
18778
+ }
18779
+ async function install(deps, input) {
18780
+ const path = deps.unitPath;
18781
+ if (existsSync2(path) && !input.force) {
18782
+ throw new Error(`${path} already exists; pass --force to replace it`);
18783
+ }
18784
+ mkdirSync(dirname(path), { recursive: true });
18785
+ writeFileSync(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
18786
+ const reload = await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
18787
+ let enabled = false;
18788
+ if (input.enable) {
18789
+ const result = await deps.run(systemctlArgs(deps.scope, ["enable", UNIT_NAME2]));
18790
+ enabled = result.code === 0;
18791
+ }
18792
+ return { path, reloaded: reload.code === 0, enabled };
18793
+ }
18794
+ async function uninstall(deps) {
18795
+ const path = deps.unitPath;
18796
+ if (!existsSync2(path))
18797
+ return { path, removed: false };
18798
+ await deps.run(systemctlArgs(deps.scope, ["disable", "--now", UNIT_NAME2]));
18799
+ rmSync(path, { force: true });
18800
+ await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
18801
+ return { path, removed: true };
18802
+ }
18803
+ function consoleSource(deps) {
18804
+ const configured = deps.logFile?.trim();
18805
+ const chosen = configured !== undefined && configured.length > 0 && fileExists(configured) ? configured : unitInstalled(deps) ? undefined : fileExists(supervisedLogFile(deps)) ? supervisedLogFile(deps) : undefined;
18806
+ return {
18807
+ source: resolveConsoleSource({
18808
+ ...chosen === undefined ? {} : { logFile: chosen },
18809
+ unitInstalled: unitInstalled(deps),
18810
+ scope: deps.scope
18811
+ }),
18812
+ deps: {
18813
+ readFile: (path, lines) => tailFile(path, lines),
18814
+ run: deps.run
18815
+ }
18816
+ };
18817
+ }
18818
+ async function serviceLogs(deps, lines) {
18819
+ const { source, deps: readDeps } = consoleSource(deps);
18820
+ const read = await readConsole(readDeps, source, { lines });
18821
+ return read.lines.map((line) => line.raw).join(`
18822
+ `);
18823
+ }
18824
+
18825
+ // apps/cli/src/commands/console.ts
18826
+ var FOLLOW_INTERVAL_MS = 2000;
18827
+ var LEVEL_TONE = {
18828
+ debug: "dim",
18829
+ warn: "yellow",
18830
+ error: "red"
18831
+ };
18832
+ function render(ctx, lines) {
18833
+ return lines.map((line) => {
18834
+ const tone = line.level === null ? undefined : LEVEL_TONE[line.level];
18835
+ return tone === undefined ? line.raw : paint(ctx, tone, line.raw);
18836
+ }).join(`
18837
+ `);
18838
+ }
18839
+ function sourceHint(source) {
18840
+ if (source.kind === "file")
18841
+ return `${source.path} (named by OMNI_LOG_FILE)`;
18842
+ if (source.kind === "journal")
18843
+ return `systemd journal for ${source.unit}`;
18844
+ return "not captured \u2014 run under systemd, or start it with `omni start`";
18845
+ }
18846
+ function empty(source) {
18847
+ if (source.kind !== "none")
18848
+ return `no output yet in ${sourceHint(source)}`;
18849
+ return [
18850
+ "this gateway's output is not being captured, so there is nothing to show.",
18851
+ "run it in the foreground and its output goes to your terminal instead.",
18852
+ "to capture it: `omni service install`, or `omni start`, which redirects",
18853
+ "output to a file and points OMNI_LOG_FILE at it."
18854
+ ].join(`
18855
+ `);
18856
+ }
18857
+ var console_ = {
18858
+ usage: "console [-n N] [--follow] [--level debug|info|warn|error]",
18859
+ summary: "Show the gateway process's own output",
18860
+ options: {
18861
+ number: { type: "string", short: "n" },
18862
+ follow: { type: "boolean" },
18863
+ level: { type: "string" },
18864
+ system: { type: "boolean" }
18865
+ },
18866
+ async run(args, { ctx, writer, service }) {
18867
+ const lines = numberFlag(args.values, "number") ?? 50;
18868
+ const rawLevel = stringFlag(args.values, "level");
18869
+ const level = parseLogLevel(rawLevel);
18870
+ if (rawLevel !== undefined && level === null) {
18871
+ throw new UsageError(`unknown level "${rawLevel}"; use debug, info, warn or error`);
18872
+ }
18873
+ const { source, deps } = consoleSource(service());
18874
+ const query = { lines, ...level === null ? {} : { level } };
18875
+ const first = await readConsole(deps, source, query);
18876
+ emit(ctx, writer, first, () => first.lines.length === 0 ? empty(source) : render(ctx, first.lines));
18877
+ if (!ctx.json && first.lines.length > 0) {
18878
+ note(ctx, writer, paint(ctx, "dim", `reading ${sourceHint(source)}`));
18879
+ }
18880
+ if (boolFlag(args.values, "follow") !== true || ctx.json)
18881
+ return;
18882
+ let seen = first.lines.at(-1)?.at ?? 0;
18883
+ for (;; ) {
18884
+ await Bun.sleep(FOLLOW_INTERVAL_MS);
18885
+ const next = await readConsole(deps, source, { ...query, since: seen });
18886
+ if (next.lines.length === 0)
18887
+ continue;
18888
+ seen = next.lines.at(-1)?.at ?? seen;
18889
+ writer.out(render(ctx, next.lines));
18890
+ }
18891
+ }
18892
+ };
18893
+
18350
18894
  // apps/cli/src/command.ts
18351
18895
  var PROVIDER_TONE = {
18352
18896
  anthropic: "magenta",
@@ -18387,7 +18931,7 @@ var credentialsList = {
18387
18931
  { header: "STATE" },
18388
18932
  { header: "EXPIRES" }
18389
18933
  ], credentials.map((c) => {
18390
- const status = condition(c);
18934
+ const status2 = condition(c);
18391
18935
  return [
18392
18936
  c.id,
18393
18937
  provider(ctx, c.provider),
@@ -18395,7 +18939,7 @@ var credentialsList = {
18395
18939
  c.authType,
18396
18940
  String(c.tier),
18397
18941
  String(c.weight),
18398
- state(ctx, status.ok, status.text),
18942
+ state(ctx, status2.ok, status2.text),
18399
18943
  formatTime(c.expiresAt)
18400
18944
  ];
18401
18945
  }));
@@ -18411,7 +18955,7 @@ var credentialsShow = {
18411
18955
  async run(args, { ctx, writer }) {
18412
18956
  const id = requirePositional(args, 0, "credential id");
18413
18957
  const credential = await findCredential(ctx, id);
18414
- const status = condition(credential);
18958
+ const status2 = condition(credential);
18415
18959
  const data = {
18416
18960
  id: credential.id,
18417
18961
  provider: credential.provider,
@@ -18434,7 +18978,7 @@ var credentialsShow = {
18434
18978
  ["provider", provider(ctx, credential.provider)],
18435
18979
  ["label", credential.label],
18436
18980
  ["auth", credential.authType],
18437
- ["state", state(ctx, status.ok, status.text)],
18981
+ ["state", state(ctx, status2.ok, status2.text)],
18438
18982
  ["tier", String(credential.tier)],
18439
18983
  ["weight", String(credential.weight)],
18440
18984
  ["account", credential.accountEmail ?? "\u2014"],
@@ -18569,12 +19113,12 @@ var credentialsHealth = {
18569
19113
  };
18570
19114
 
18571
19115
  // apps/cli/src/commands/db.ts
18572
- import { existsSync as existsSync2 } from "fs";
19116
+ import { existsSync as existsSync3 } from "fs";
18573
19117
  var dbMigrate = {
18574
19118
  usage: "db migrate",
18575
19119
  summary: "Create or upgrade the database schema",
18576
19120
  async run(_args, { ctx, writer }) {
18577
- const existed = existsSync2(ctx.databasePath);
19121
+ const existed = existsSync3(ctx.databasePath);
18578
19122
  const store = await ctx.store();
18579
19123
  await store.config.getSettings();
18580
19124
  emit(ctx, writer, { path: ctx.databasePath, created: !existed }, () => fields([
@@ -18696,6 +19240,11 @@ var modelsShow = {
18696
19240
  { header: "WEIGHT", align: "right" },
18697
19241
  { header: "IN $/MTOK", align: "right" },
18698
19242
  { header: "OUT $/MTOK", align: "right" },
19243
+ { header: "CACHE R", align: "right" },
19244
+ { header: "CACHE W 5M", align: "right" },
19245
+ { header: "CACHE W 1H", align: "right" },
19246
+ { header: "CONTEXT", align: "right" },
19247
+ { header: "MAX OUT", align: "right" },
18699
19248
  { header: "CAPABILITIES" }
18700
19249
  ], model.targets.map((target) => [
18701
19250
  provider(ctx, target.provider),
@@ -18704,12 +19253,23 @@ var modelsShow = {
18704
19253
  String(target.weight),
18705
19254
  target.costPerMTok.input.toFixed(2),
18706
19255
  target.costPerMTok.output.toFixed(2),
19256
+ price(target.costPerMTok.cacheRead),
19257
+ price(target.costPerMTok.cacheWrite5m),
19258
+ price(target.costPerMTok.cacheWrite1h),
19259
+ tokens(target.contextWindow),
19260
+ tokens(target.maxOutputTokens),
18707
19261
  capabilityList(target)
18708
19262
  ]))
18709
19263
  ].join(`
18710
19264
  `));
18711
19265
  }
18712
19266
  };
19267
+ function price(value) {
19268
+ return value === undefined ? "\u2014" : value.toFixed(2);
19269
+ }
19270
+ function tokens(value) {
19271
+ return value === undefined ? "\u2014" : value.toLocaleString("en-US");
19272
+ }
18713
19273
  function capabilityList(target) {
18714
19274
  const on = Object.entries(target.capabilities).filter(([, enabled]) => enabled).map(([name]) => name);
18715
19275
  return on.length === 0 ? "\u2014" : on.join(",");
@@ -18741,7 +19301,7 @@ function isCatalogProvider(value) {
18741
19301
  return value in PROVIDER_MODEL_CATALOG;
18742
19302
  }
18743
19303
  function catalogRows() {
18744
- return Object.entries(PROVIDER_MODEL_CATALOG).flatMap(([id, entry]) => entry.models.map((model) => ({ provider: id, id: model.id, label: model.label })));
19304
+ return Object.entries(PROVIDER_MODEL_CATALOG).flatMap(([id, entry]) => entry.models.map((model) => ({ ...model, provider: id })));
18745
19305
  }
18746
19306
  var modelsCatalog = {
18747
19307
  usage: "models catalog [--provider P]",
@@ -18755,15 +19315,29 @@ var modelsCatalog = {
18755
19315
  { header: "MODEL" },
18756
19316
  { header: "LABEL" },
18757
19317
  { header: "IN $/MTOK", align: "right" },
18758
- { header: "OUT $/MTOK", align: "right" }
19318
+ { header: "OUT $/MTOK", align: "right" },
19319
+ { header: "CACHE R", align: "right" },
19320
+ { header: "CACHE W 5M", align: "right" },
19321
+ { header: "CACHE W 1H", align: "right" },
19322
+ { header: "CONTEXT", align: "right" },
19323
+ { header: "CONTEXT OAUTH", align: "right" },
19324
+ { header: "MAX OUT", align: "right" }
18759
19325
  ], entries.map((entry) => {
18760
- const price = catalogPricing(entry.provider, entry.id);
19326
+ const listed = catalogPricing(entry.provider, entry.id);
19327
+ const limits = catalogLimits(entry.provider, entry.id);
19328
+ const oauth = catalogLimits(entry.provider, entry.id, "oauth");
18761
19329
  return [
18762
19330
  provider(ctx, entry.provider),
18763
19331
  entry.id,
18764
19332
  entry.label,
18765
- price === null ? "\u2014" : price.input.toFixed(2),
18766
- price === null ? "\u2014" : price.output.toFixed(2)
19333
+ price(listed?.input),
19334
+ price(listed?.output),
19335
+ price(listed?.cacheRead),
19336
+ price(listed?.cacheWrite5m),
19337
+ price(listed?.cacheWrite1h),
19338
+ tokens(limits?.contextWindow),
19339
+ oauth?.contextWindow === limits?.contextWindow ? "\u2014" : tokens(oauth?.contextWindow),
19340
+ tokens(limits?.maxOutputTokens)
18767
19341
  ];
18768
19342
  })));
18769
19343
  }
@@ -18872,207 +19446,8 @@ ${candidates}${excluded}`;
18872
19446
  import { existsSync as existsSync5 } from "fs";
18873
19447
 
18874
19448
  // apps/cli/src/runtime.ts
18875
- import { closeSync, existsSync as existsSync4, openSync } from "fs";
19449
+ import { closeSync as closeSync2, existsSync as existsSync4, openSync as openSync2 } from "fs";
18876
19450
  import { join as join3 } from "path";
18877
-
18878
- // apps/cli/src/service.ts
18879
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
18880
- import { homedir as homedir2 } from "os";
18881
- import { dirname, join as join2 } from "path";
18882
- var UNIT_NAME = "omnigateway.service";
18883
- function unitPath(scope) {
18884
- return scope === "system" ? join2("/etc/systemd/system", UNIT_NAME) : join2(homedir2(), ".config", "systemd", "user", UNIT_NAME);
18885
- }
18886
- function systemctlArgs(scope, args) {
18887
- return scope === "system" ? ["systemctl", ...args] : ["systemctl", "--user", ...args];
18888
- }
18889
- function pidFile(stateDir) {
18890
- return join2(stateDir, "gateway.pid");
18891
- }
18892
- function logFile(stateDir) {
18893
- return join2(stateDir, "gateway.log");
18894
- }
18895
- function defaultStateDir(env2) {
18896
- const xdg = env2.XDG_STATE_HOME;
18897
- const base = typeof xdg === "string" && xdg.length > 0 ? xdg : join2(homedir2(), ".local", "state");
18898
- return join2(base, "omnigateway");
18899
- }
18900
- function unitFile(input) {
18901
- return `[Unit]
18902
- Description=${input.description ?? "OmniGateway"}
18903
- After=network-online.target
18904
- Wants=network-online.target
18905
-
18906
- [Service]
18907
- Type=simple
18908
- WorkingDirectory=${input.root}
18909
- EnvironmentFile=${join2(input.root, ".env")}
18910
- ExecStart=${input.bun} ${input.entrypoint}
18911
- Restart=on-failure
18912
- RestartSec=2
18913
-
18914
- [Install]
18915
- WantedBy=default.target
18916
- `;
18917
- }
18918
- function unitInstalled(deps) {
18919
- return existsSync3(deps.unitPath);
18920
- }
18921
- function readPid(stateDir) {
18922
- const file2 = pidFile(stateDir);
18923
- if (!existsSync3(file2))
18924
- return null;
18925
- const pid = Number(readFileSync2(file2, "utf8").trim());
18926
- return Number.isInteger(pid) && pid > 0 ? pid : null;
18927
- }
18928
- function livePid(deps) {
18929
- const pid = readPid(deps.stateDir);
18930
- if (pid === null)
18931
- return null;
18932
- if (deps.alive(pid))
18933
- return pid;
18934
- rmSync(pidFile(deps.stateDir), { force: true });
18935
- return null;
18936
- }
18937
- async function status(deps) {
18938
- if (unitInstalled(deps)) {
18939
- const active = await deps.run(systemctlArgs(deps.scope, ["is-active", UNIT_NAME]));
18940
- const state2 = active.stdout.trim() || active.stderr.trim() || "unknown";
18941
- const shown = await deps.run(systemctlArgs(deps.scope, ["show", UNIT_NAME, "--property=MainPID", "--value"]));
18942
- const pid2 = Number(shown.stdout.trim());
18943
- return {
18944
- supervisor: "systemd",
18945
- running: state2 === "active",
18946
- pid: Number.isInteger(pid2) && pid2 > 0 ? pid2 : null,
18947
- state: state2,
18948
- unitPath: deps.unitPath,
18949
- logFile: null
18950
- };
18951
- }
18952
- const pid = livePid(deps);
18953
- return {
18954
- supervisor: pid === null ? "none" : "pidfile",
18955
- running: pid !== null,
18956
- pid,
18957
- state: null,
18958
- unitPath: null,
18959
- logFile: logFile(deps.stateDir)
18960
- };
18961
- }
18962
- async function start(deps, input) {
18963
- if (unitInstalled(deps)) {
18964
- const result = await deps.run(systemctlArgs(deps.scope, ["start", UNIT_NAME]));
18965
- if (result.code !== 0) {
18966
- throw new Error(result.stderr.trim() || `systemctl start exited ${result.code}`);
18967
- }
18968
- return {
18969
- supervisor: "systemd",
18970
- pid: null,
18971
- healthy: await waitForHealth(deps, input.baseUrl)
18972
- };
18973
- }
18974
- const running = livePid(deps);
18975
- if (running !== null) {
18976
- return {
18977
- supervisor: "pidfile",
18978
- pid: running,
18979
- healthy: await waitForHealth(deps, input.baseUrl)
18980
- };
18981
- }
18982
- mkdirSync(deps.stateDir, { recursive: true });
18983
- const pid = deps.spawn({
18984
- argv: input.argv,
18985
- cwd: deps.root,
18986
- env: input.env,
18987
- logFile: logFile(deps.stateDir)
18988
- });
18989
- writeFileSync(pidFile(deps.stateDir), `${pid}
18990
- `);
18991
- return { supervisor: "pidfile", pid, healthy: await waitForHealth(deps, input.baseUrl) };
18992
- }
18993
- var HEALTH_TIMEOUT_MS = 1e4;
18994
- var HEALTH_INTERVAL_MS = 200;
18995
- async function waitForHealth(deps, baseUrl) {
18996
- const attempts = Math.ceil(HEALTH_TIMEOUT_MS / HEALTH_INTERVAL_MS);
18997
- for (let attempt = 0;attempt < attempts; attempt++) {
18998
- if (await deps.probe(baseUrl))
18999
- return true;
19000
- await deps.sleep(HEALTH_INTERVAL_MS);
19001
- }
19002
- return false;
19003
- }
19004
- var STOP_GRACE_MS = 5000;
19005
- var STOP_INTERVAL_MS = 100;
19006
- async function stop(deps) {
19007
- if (unitInstalled(deps)) {
19008
- const result = await deps.run(systemctlArgs(deps.scope, ["stop", UNIT_NAME]));
19009
- if (result.code !== 0) {
19010
- throw new Error(result.stderr.trim() || `systemctl stop exited ${result.code}`);
19011
- }
19012
- return { supervisor: "systemd", stopped: true };
19013
- }
19014
- const pid = livePid(deps);
19015
- if (pid === null)
19016
- return { supervisor: "none", stopped: false };
19017
- deps.kill(pid, "SIGTERM");
19018
- const attempts = Math.ceil(STOP_GRACE_MS / STOP_INTERVAL_MS);
19019
- for (let attempt = 0;deps.alive(pid); attempt++) {
19020
- if (attempt >= attempts) {
19021
- deps.kill(pid, "SIGKILL");
19022
- break;
19023
- }
19024
- await deps.sleep(STOP_INTERVAL_MS);
19025
- }
19026
- rmSync(pidFile(deps.stateDir), { force: true });
19027
- return { supervisor: "pidfile", stopped: true };
19028
- }
19029
- async function install(deps, input) {
19030
- const path = deps.unitPath;
19031
- if (existsSync3(path) && !input.force) {
19032
- throw new Error(`${path} already exists; pass --force to replace it`);
19033
- }
19034
- mkdirSync(dirname(path), { recursive: true });
19035
- writeFileSync(path, unitFile({ root: deps.root, bun: input.bun, entrypoint: input.entrypoint }));
19036
- const reload = await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
19037
- let enabled = false;
19038
- if (input.enable) {
19039
- const result = await deps.run(systemctlArgs(deps.scope, ["enable", UNIT_NAME]));
19040
- enabled = result.code === 0;
19041
- }
19042
- return { path, reloaded: reload.code === 0, enabled };
19043
- }
19044
- async function uninstall(deps) {
19045
- const path = deps.unitPath;
19046
- if (!existsSync3(path))
19047
- return { path, removed: false };
19048
- await deps.run(systemctlArgs(deps.scope, ["disable", "--now", UNIT_NAME]));
19049
- rmSync(path, { force: true });
19050
- await deps.run(systemctlArgs(deps.scope, ["daemon-reload"]));
19051
- return { path, removed: true };
19052
- }
19053
- async function serviceLogs(deps, lines) {
19054
- if (unitInstalled(deps)) {
19055
- const args = deps.scope === "system" ? [] : ["--user"];
19056
- const result = await deps.run([
19057
- "journalctl",
19058
- ...args,
19059
- "-u",
19060
- UNIT_NAME,
19061
- "-n",
19062
- String(lines),
19063
- "--no-pager"
19064
- ]);
19065
- return result.stdout.trimEnd();
19066
- }
19067
- const file2 = logFile(deps.stateDir);
19068
- if (!existsSync3(file2))
19069
- return "";
19070
- return readFileSync2(file2, "utf8").split(`
19071
- `).slice(-lines).join(`
19072
- `).trimEnd();
19073
- }
19074
-
19075
- // apps/cli/src/runtime.ts
19076
19451
  var runCommand = async (argv) => {
19077
19452
  const proc = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
19078
19453
  const [stdout, stderr, code] = await Promise.all([
@@ -19083,7 +19458,7 @@ var runCommand = async (argv) => {
19083
19458
  return { code, stdout, stderr };
19084
19459
  };
19085
19460
  var spawnDetached = ({ argv, cwd, env: env2, logFile: logFile2 }) => {
19086
- const fd = openSync(logFile2, "a");
19461
+ const fd = openSync2(logFile2, "a");
19087
19462
  try {
19088
19463
  const proc = Bun.spawn([...argv], {
19089
19464
  cwd,
@@ -19095,7 +19470,7 @@ var spawnDetached = ({ argv, cwd, env: env2, logFile: logFile2 }) => {
19095
19470
  proc.unref();
19096
19471
  return proc.pid;
19097
19472
  } finally {
19098
- closeSync(fd);
19473
+ closeSync2(fd);
19099
19474
  }
19100
19475
  };
19101
19476
  async function runForeground(input) {
@@ -19136,11 +19511,13 @@ function gatewayEntrypoint(root, cliDir = import.meta.dir) {
19136
19511
  return candidates.find((path) => existsSync4(path)) ?? null;
19137
19512
  }
19138
19513
  function createServiceDeps(input) {
19514
+ const logFile2 = input.env.OMNI_LOG_FILE?.trim();
19139
19515
  return {
19140
19516
  root: input.root,
19141
19517
  stateDir: defaultStateDir(input.env),
19142
19518
  scope: input.scope,
19143
19519
  unitPath: unitPath(input.scope),
19520
+ ...logFile2 === undefined || logFile2.length === 0 ? {} : { logFile: logFile2 },
19144
19521
  run: runCommand,
19145
19522
  spawn: spawnDetached,
19146
19523
  probe: probeHealth,
@@ -19183,7 +19560,7 @@ var start2 = {
19183
19560
  emit(ctx, writer, result, () => {
19184
19561
  const how = result.supervisor === "systemd" ? "systemd" : `pid ${result.pid}`;
19185
19562
  if (!result.healthy) {
19186
- const where = result.supervisor === "systemd" ? "journalctl -u omnigateway.service" : logFile(deps.stateDir);
19563
+ const where = result.supervisor === "systemd" ? "journalctl -u omnigateway.service" : supervisedLogFile(deps);
19187
19564
  return `${state(ctx, false, "started but /health never answered")} (${how}); see ${where}`;
19188
19565
  }
19189
19566
  return `${state(ctx, true, "running")} (${how}) at ${baseUrl}`;
@@ -19276,7 +19653,8 @@ var doctor = {
19276
19653
  gatewayEntrypoint: gatewayEntrypoint(deps.root),
19277
19654
  unitInstalled: unit,
19278
19655
  supervisor: status2.supervisor,
19279
- running: status2.running
19656
+ running: status2.running,
19657
+ consoleSource: consoleSource(deps).source
19280
19658
  };
19281
19659
  emit(ctx, writer, checks3, () => {
19282
19660
  const ok = (value, text) => state(ctx, value, text);
@@ -19291,7 +19669,8 @@ var doctor = {
19291
19669
  ["config", checks3.configError === null ? ok(true, "ok") : ok(false, checks3.configError)],
19292
19670
  ["entrypoint", checks3.gatewayEntrypoint ?? ok(false, "not found")],
19293
19671
  ["systemd unit", unit ? deps.scope : paint(ctx, "dim", "none")],
19294
- ["gateway", ok(status2.running, status2.running ? "running" : "stopped")]
19672
+ ["gateway", ok(status2.running, status2.running ? "running" : "stopped")],
19673
+ ["console log", sourceHint(checks3.consoleSource)]
19295
19674
  ]);
19296
19675
  });
19297
19676
  }
@@ -19495,11 +19874,12 @@ var usage = {
19495
19874
  };
19496
19875
  var logs = {
19497
19876
  usage: "logs [-n N] [--follow] [--service]",
19498
- summary: "Show recent requests, or the gateway process's own output",
19877
+ summary: "Show recent requests (--service is an alias for `omni console`)",
19499
19878
  options: {
19500
19879
  number: { type: "string", short: "n" },
19501
19880
  follow: { type: "boolean" },
19502
- service: { type: "boolean" }
19881
+ service: { type: "boolean" },
19882
+ system: { type: "boolean" }
19503
19883
  },
19504
19884
  async run(args, { ctx, writer, service }) {
19505
19885
  const limit = numberFlag(args.values, "number") ?? 20;
@@ -19509,7 +19889,7 @@ var logs = {
19509
19889
  return;
19510
19890
  }
19511
19891
  const store = await ctx.store();
19512
- const render = (rows) => table([
19892
+ const render2 = (rows) => table([
19513
19893
  { header: "AT" },
19514
19894
  { header: "MODEL" },
19515
19895
  { header: "RESOLVED" },
@@ -19527,7 +19907,7 @@ var logs = {
19527
19907
  String(row.durationMs)
19528
19908
  ]));
19529
19909
  const first = await recentLogs(store, limit);
19530
- emit(ctx, writer, { logs: first }, () => first.length === 0 ? "no requests logged yet" : render(first));
19910
+ emit(ctx, writer, { logs: first }, () => first.length === 0 ? "no requests logged yet" : render2(first));
19531
19911
  if (args.values.follow !== true || ctx.json)
19532
19912
  return;
19533
19913
  let seen = first[0]?.at ?? 0;
@@ -19537,7 +19917,7 @@ var logs = {
19537
19917
  if (next.length === 0)
19538
19918
  continue;
19539
19919
  seen = next[0]?.at ?? seen;
19540
- writer.out(render(next.reverse()));
19920
+ writer.out(render2(next.reverse()));
19541
19921
  }
19542
19922
  }
19543
19923
  };
@@ -19550,6 +19930,7 @@ var COMMANDS = {
19550
19930
  restart,
19551
19931
  doctor,
19552
19932
  logs,
19933
+ console: console_,
19553
19934
  usage,
19554
19935
  connect,
19555
19936
  "service install": serviceInstall,
@@ -19612,7 +19993,10 @@ function resolveCommand(argv) {
19612
19993
 
19613
19994
  // apps/cli/src/help.ts
19614
19995
  var GROUPS = [
19615
- { title: "Gateway", prefixes: ["status", "start", "stop", "restart", "doctor", "logs"] },
19996
+ {
19997
+ title: "Gateway",
19998
+ prefixes: ["status", "start", "stop", "restart", "doctor", "logs", "console"]
19999
+ },
19616
20000
  { title: "Service", prefixes: ["service "] },
19617
20001
  { title: "Accounts", prefixes: ["connect", "credentials "] },
19618
20002
  { title: "Models", prefixes: ["models "] },