@jacobbd/relay-ai 0.3.1 → 0.3.2

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 (3) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli.js +270 -90
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] - 2026-06-22
4
+
5
+ ### Fixed
6
+
7
+ - **Codex App: rate limit errors now appear in the conversation instead of crashing silently** — when a model hits its usage limit (e.g. OpenCode Go's 5-hour cap), the proxy now injects a readable error message directly into the Codex App conversation: `"5-hour usage limit reached. Resets in Xmin. To continue using this model now, enable usage from your available balance: ..."`. Previously the session just stalled with no explanation in the UI.
8
+
9
+ - **Codex App: rate limit errors print a clean one-liner in the terminal** — instead of flooding the terminal with full RetryError stack traces (one per retry attempt, per request), the proxy now prints a single `[relay-ai] <model>: <message>` line per failed request.
10
+
11
+ - **Codex proxy: removed SDK default `console.error` on stream failures** — the Vercel AI SDK's `streamText` calls `console.error(error)` by default whenever the stream encounters an error. This was the root cause of the full stack trace dumps. The proxy now passes `onError: () => {}` to suppress this. The error is still handled through the stream pipeline and surfaced to the user.
12
+
13
+ - **Codex App: context overflow no longer crashes long sessions** — relay-ai now writes `model_context_window` and `model_auto_compact_token_limit` (70% of the model's actual limit) into `~/.codex/config.toml` at session start. Codex uses these values to trigger auto-compaction before the conversation reaches the model's hard limit, preventing the compaction-fails-at-limit crash that previously broke sessions and made them unrecoverable. Applies to single-provider, favorites, and Vertex AI sessions alike.
14
+
15
+ - **Codex App: proxy-level message truncation as a safety net** — if a conversation history arrives that already exceeds 85% of the selected model's context window (e.g. a long native GPT-5.5 session loaded into a 1 M-token model), relay-ai silently drops the oldest messages before forwarding to the upstream model. The session continues in a degraded but functional state instead of crashing with an unrecoverable error.
16
+
17
+ - **Codex App: Ctrl+C now shows a confirmation menu instead of immediately closing** — pressing Ctrl+C now presents an arrow-key selection menu: *"Close Codex Desktop and restore your Codex config?"* (Yes / No). Pressing Ctrl+C a second time during the prompt, or pressing Enter on Yes, closes the app and restores config. Choosing No keeps the session running. SIGTERM and SIGHUP still close immediately without a prompt.
18
+
19
+ - **Codex App: `--trace` request observability** — `--trace` mode now logs `previous_response_id`, `input_items`, and `body_bytes` for every incoming proxy request, making it possible to verify Codex's conversation-history protocol against a specific provider setup.
20
+
3
21
  ## [0.3.1] - 2026-06-22
4
22
 
5
23
  ### Fixed
package/dist/cli.js CHANGED
@@ -35,7 +35,7 @@ import { join } from "path";
35
35
  // package.json
36
36
  var package_default = {
37
37
  name: "@jacobbd/relay-ai",
38
- version: "0.3.1",
38
+ version: "0.3.2",
39
39
  publishConfig: {
40
40
  access: "public"
41
41
  },
@@ -832,11 +832,16 @@ function parseCodexAppModelSlug(modelKey) {
832
832
  }
833
833
  function buildCodexAppRootConfig(spec) {
834
834
  const slug = codexAppModelSlug(spec.route.modelId);
835
+ const ctxWindow = spec.route.contextWindow;
835
836
  return {
836
837
  model: slug,
837
838
  model_provider: "openai",
838
839
  openai_base_url: `http://127.0.0.1:${spec.proxyPort}/v1`,
839
- model_catalog_json: spec.catalogPath
840
+ model_catalog_json: spec.catalogPath,
841
+ ...ctxWindow && ctxWindow > 0 ? {
842
+ model_context_window: ctxWindow,
843
+ model_auto_compact_token_limit: Math.floor(ctxWindow * 0.7)
844
+ } : {}
840
845
  };
841
846
  }
842
847
 
@@ -5594,7 +5599,8 @@ async function writeAnthropicStream(fullStream, modelId, write, log19) {
5594
5599
  emit("message_stop", { type: "message_stop" });
5595
5600
  }
5596
5601
  async function streamAnthropicResponse(model, params, modelId, write, log19) {
5597
- const result = streamText({ model, ...params });
5602
+ const result = streamText({ model, ...params, onError: () => {
5603
+ } });
5598
5604
  Promise.resolve(result.text).catch(() => {
5599
5605
  });
5600
5606
  Promise.resolve(result.toolCalls).catch(() => {
@@ -9110,11 +9116,11 @@ async function runProvidersRemove(id, interactive = false) {
9110
9116
  return 1;
9111
9117
  }
9112
9118
  if (interactive) {
9113
- const confirm10 = await p10.confirm({
9119
+ const confirm9 = await p10.confirm({
9114
9120
  message: `Remove ${provider.name} (${id})?`,
9115
9121
  initialValue: false
9116
9122
  });
9117
- if (p10.isCancel(confirm10) || !confirm10) {
9123
+ if (p10.isCancel(confirm9) || !confirm9) {
9118
9124
  p10.cancel("Cancelled.");
9119
9125
  return 0;
9120
9126
  }
@@ -9308,6 +9314,53 @@ import { createServer as createServer3 } from "http";
9308
9314
 
9309
9315
  // src/codex-responses-adapter.ts
9310
9316
  import { streamText as streamText3, generateText as generateText3, tool as tool3, jsonSchema as jsonSchema3 } from "ai";
9317
+
9318
+ // src/codex/upstream-error.ts
9319
+ function formatUpstreamError(err) {
9320
+ if (!err || typeof err !== "object") return "Upstream model request failed.";
9321
+ const rec = err;
9322
+ if (rec.data?.error?.message) {
9323
+ const short = sanitizeMessage(rec.data.error.message);
9324
+ return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
9325
+ }
9326
+ if (rec.responseBody) {
9327
+ try {
9328
+ const parsed = JSON.parse(rec.responseBody);
9329
+ if (parsed.error?.message) {
9330
+ const short = sanitizeMessage(parsed.error.message);
9331
+ return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
9332
+ }
9333
+ } catch {
9334
+ }
9335
+ }
9336
+ const last = rec.lastError;
9337
+ if (last?.message) {
9338
+ const code = last.statusCode;
9339
+ const short = sanitizeMessage(last.message);
9340
+ return code ? `${short} (HTTP ${code})` : short;
9341
+ }
9342
+ const fromList = rec.errors?.[rec.errors.length - 1];
9343
+ if (fromList?.message) {
9344
+ const short = sanitizeMessage(fromList.message);
9345
+ return fromList.statusCode ? `${short} (HTTP ${fromList.statusCode})` : short;
9346
+ }
9347
+ if (rec.message) {
9348
+ const short = sanitizeMessage(rec.message);
9349
+ if (short && !short.includes("file://") && !short.includes("APICallError") && short.length < 240) {
9350
+ return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
9351
+ }
9352
+ }
9353
+ return "Upstream model request failed.";
9354
+ }
9355
+ function sanitizeMessage(message) {
9356
+ const line = message.split("\n")[0]?.trim() ?? message;
9357
+ if (line.startsWith("RetryError") || line.includes("AI_RetryError")) {
9358
+ return "Upstream model request failed after retries.";
9359
+ }
9360
+ return line;
9361
+ }
9362
+
9363
+ // src/codex-responses-adapter.ts
9311
9364
  function messageText(content) {
9312
9365
  if (typeof content === "string") return content;
9313
9366
  return (content ?? []).map((p21) => p21.type === "output_text" || p21.type === "input_text" || p21.type === "text" ? p21.text ?? "" : "").join("");
@@ -9620,20 +9673,29 @@ async function writeResponsesStream(fullStream, modelId, write) {
9620
9673
  case "finish":
9621
9674
  if (part.totalUsage) usage = usageFromPart(part);
9622
9675
  break;
9623
- case "error":
9624
- emit("response.completed", {
9625
- type: "response.completed",
9626
- response: {
9627
- id: responseId,
9628
- object: "response",
9629
- model: modelId,
9630
- created_at: createdAt,
9631
- status: "failed",
9632
- output: [],
9633
- error: { message: String(part.error ?? "Upstream error"), type: "api_error" }
9634
- }
9635
- });
9676
+ case "error": {
9677
+ const msg = formatUpstreamError(part.error);
9678
+ const is429 = msg.includes("429") || part.error && typeof part.error === "object" && (part.error.statusCode === 429 || part.error.lastError?.statusCode === 429);
9679
+ process.stderr.write(`[relay-ai] ${modelId}: ${msg}
9680
+ `);
9681
+ if (is429) {
9682
+ writeResponsesRateLimitStream(modelId, msg, write);
9683
+ } else {
9684
+ emit("response.completed", {
9685
+ type: "response.completed",
9686
+ response: {
9687
+ id: responseId,
9688
+ object: "response",
9689
+ model: modelId,
9690
+ created_at: createdAt,
9691
+ status: "failed",
9692
+ output: [],
9693
+ error: { message: msg, type: "api_error" }
9694
+ }
9695
+ });
9696
+ }
9636
9697
  return;
9698
+ }
9637
9699
  default:
9638
9700
  break;
9639
9701
  }
@@ -9712,7 +9774,8 @@ async function writeResponsesStream(fullStream, modelId, write) {
9712
9774
  });
9713
9775
  }
9714
9776
  async function streamResponsesResponse(model, params, modelId, write) {
9715
- const result = streamText3({ model, ...params });
9777
+ const result = streamText3({ model, ...params, onError: () => {
9778
+ } });
9716
9779
  Promise.resolve(result.text).catch(() => {
9717
9780
  });
9718
9781
  Promise.resolve(result.toolCalls).catch(() => {
@@ -9723,6 +9786,8 @@ async function streamResponsesResponse(model, params, modelId, write) {
9723
9786
  });
9724
9787
  Promise.resolve(result.usage).catch(() => {
9725
9788
  });
9789
+ Promise.resolve(result.response).catch(() => {
9790
+ });
9726
9791
  await writeResponsesStream(result.fullStream, modelId, write);
9727
9792
  }
9728
9793
  async function generateResponsesResponse(model, params, modelId) {
@@ -9786,53 +9851,102 @@ function writeResponsesErrorStream(modelId, message, write, statusCode = 401) {
9786
9851
  response: responsesErrorBody(modelId, message, statusCode)
9787
9852
  }));
9788
9853
  }
9854
+ function writeResponsesRateLimitStream(modelId, message, write) {
9855
+ const responseId = newResponseId();
9856
+ const itemId = newItemId("msg");
9857
+ const createdAt = Math.floor(Date.now() / 1e3);
9858
+ const content = [{ type: "output_text", text: message }];
9859
+ write(sseChunk("response.output_item.added", {
9860
+ type: "response.output_item.added",
9861
+ output_index: 0,
9862
+ item: { id: itemId, type: "message", role: "assistant", status: "in_progress", content: [] }
9863
+ }));
9864
+ write(sseChunk("response.content_part.added", {
9865
+ type: "response.content_part.added",
9866
+ item_id: itemId,
9867
+ output_index: 0,
9868
+ content_index: 0,
9869
+ part: { type: "output_text", text: "" }
9870
+ }));
9871
+ write(sseChunk("response.output_text.delta", {
9872
+ type: "response.output_text.delta",
9873
+ item_id: itemId,
9874
+ output_index: 0,
9875
+ content_index: 0,
9876
+ delta: message
9877
+ }));
9878
+ write(sseChunk("response.output_text.done", {
9879
+ type: "response.output_text.done",
9880
+ item_id: itemId,
9881
+ output_index: 0,
9882
+ content_index: 0,
9883
+ text: message
9884
+ }));
9885
+ write(sseChunk("response.content_part.done", {
9886
+ type: "response.content_part.done",
9887
+ item_id: itemId,
9888
+ output_index: 0,
9889
+ content_index: 0,
9890
+ part: { type: "output_text", text: message }
9891
+ }));
9892
+ write(sseChunk("response.output_item.done", {
9893
+ type: "response.output_item.done",
9894
+ output_index: 0,
9895
+ item: { id: itemId, type: "message", role: "assistant", status: "completed", content }
9896
+ }));
9897
+ write(sseChunk("response.completed", {
9898
+ type: "response.completed",
9899
+ response: {
9900
+ id: responseId,
9901
+ object: "response",
9902
+ model: modelId,
9903
+ created_at: createdAt,
9904
+ status: "completed",
9905
+ output: [{ id: itemId, type: "message", role: "assistant", status: "completed", content }]
9906
+ }
9907
+ }));
9908
+ }
9909
+ function responsesRateLimitBody(modelId, message) {
9910
+ const itemId = newItemId("msg");
9911
+ const content = [{ type: "output_text", text: message }];
9912
+ return {
9913
+ id: newResponseId(),
9914
+ object: "response",
9915
+ model: modelId,
9916
+ created_at: Math.floor(Date.now() / 1e3),
9917
+ status: "completed",
9918
+ output: [{ id: itemId, type: "message", role: "assistant", status: "completed", content }]
9919
+ };
9920
+ }
9789
9921
 
9790
- // src/codex/upstream-error.ts
9791
- function formatUpstreamError(err) {
9792
- if (!err || typeof err !== "object") return "Upstream model request failed.";
9793
- const rec = err;
9794
- if (rec.data?.error?.message) {
9795
- const short = sanitizeMessage(rec.data.error.message);
9796
- return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
9797
- }
9798
- if (rec.responseBody) {
9799
- try {
9800
- const parsed = JSON.parse(rec.responseBody);
9801
- if (parsed.error?.message) {
9802
- const short = sanitizeMessage(parsed.error.message);
9803
- return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
9922
+ // src/codex-proxy.ts
9923
+ function estimateMessageChars(params) {
9924
+ let chars = (params.system ?? "").length;
9925
+ for (const msg of params.messages) {
9926
+ if (Array.isArray(msg.content)) {
9927
+ for (const part of msg.content) {
9928
+ if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
9929
+ chars += part.text.length;
9930
+ }
9804
9931
  }
9805
- } catch {
9932
+ } else if (typeof msg.content === "string") {
9933
+ chars += msg.content.length;
9806
9934
  }
9807
9935
  }
9808
- const last = rec.lastError;
9809
- if (last?.message) {
9810
- const code = last.statusCode;
9811
- const short = sanitizeMessage(last.message);
9812
- return code ? `${short} (HTTP ${code})` : short;
9813
- }
9814
- const fromList = rec.errors?.[rec.errors.length - 1];
9815
- if (fromList?.message) {
9816
- const short = sanitizeMessage(fromList.message);
9817
- return fromList.statusCode ? `${short} (HTTP ${fromList.statusCode})` : short;
9818
- }
9819
- if (rec.message) {
9820
- const short = sanitizeMessage(rec.message);
9821
- if (short && !short.includes("file://") && !short.includes("APICallError") && short.length < 240) {
9822
- return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
9823
- }
9824
- }
9825
- return "Upstream model request failed.";
9936
+ return chars;
9826
9937
  }
9827
- function sanitizeMessage(message) {
9828
- const line = message.split("\n")[0]?.trim() ?? message;
9829
- if (line.startsWith("RetryError") || line.includes("AI_RetryError")) {
9830
- return "Upstream model request failed after retries.";
9938
+ function trimToContextLimit(params, contextWindow) {
9939
+ const charLimit = Math.floor(contextWindow * 0.85) * 4;
9940
+ if (estimateMessageChars(params) <= charLimit) return params;
9941
+ let messages = [...params.messages];
9942
+ while (messages.length > 1 && estimateMessageChars({ ...params, messages }) > charLimit) {
9943
+ messages = messages.slice(1);
9944
+ while (messages.length > 0 && messages[0].role !== "user") {
9945
+ messages = messages.slice(1);
9946
+ }
9831
9947
  }
9832
- return line;
9948
+ return { ...params, messages };
9833
9949
  }
9834
-
9835
- // src/codex-proxy.ts
9836
9950
  var PROXY_PLACEHOLDER_KEY = "proxy-local";
9837
9951
  function codexRouteLookupIds(requestedModel) {
9838
9952
  const ids = routeLookupIds(requestedModel);
@@ -9871,11 +9985,6 @@ function upstreamHttpStatus(err, msg) {
9871
9985
  if (msg.includes("HTTP 400")) return 400;
9872
9986
  return 500;
9873
9987
  }
9874
- function logUpstreamError(err, modelId) {
9875
- const msg = formatUpstreamError(err);
9876
- const prefix = modelId ? `[relay-ai codex-proxy] ${modelId}: ` : "[relay-ai codex-proxy] ";
9877
- console.error(`${prefix}${msg}`);
9878
- }
9879
9988
  function resolveModel(routes, models, requestedModel) {
9880
9989
  const route = findCodexProxyRoute(routes, requestedModel);
9881
9990
  if (!route) return void 0;
@@ -9905,8 +10014,7 @@ async function startCodexProxy(routes, options = {}) {
9905
10014
  const log19 = debug ? makeTraceLogger(getCodexProxyDebugLogPath()) : () => {
9906
10015
  };
9907
10016
  const onRejection = (reason) => {
9908
- logUpstreamError(reason);
9909
- if (debug) log19(formatUpstreamError(reason));
10017
+ if (debug) log19(`unhandled-rejection: ${formatUpstreamError(reason)}`);
9910
10018
  };
9911
10019
  process.on("unhandledRejection", onRejection);
9912
10020
  const server = createServer3(async (req, res) => {
@@ -10006,6 +10114,11 @@ async function startCodexProxy(routes, options = {}) {
10006
10114
  sendJson(res, 400, { error: { message: "Invalid JSON body", type: "invalid_request_error" } });
10007
10115
  return;
10008
10116
  }
10117
+ if (debug) {
10118
+ const prevId = body.previous_response_id ?? null;
10119
+ const inputItems = Array.isArray(body.input) ? body.input.length : typeof body.input === "string" ? 1 : 0;
10120
+ log19(`request: model=${String(body.model ?? "")} previous_response_id=${prevId ?? "(none)"} input_items=${inputItems} body_bytes=${rawBody.length}`);
10121
+ }
10009
10122
  const modelId = String(body.model ?? "");
10010
10123
  let resolved = resolveModel(routes, models, modelId);
10011
10124
  if (!resolved) {
@@ -10026,7 +10139,7 @@ async function startCodexProxy(routes, options = {}) {
10026
10139
  }
10027
10140
  const { route, languageModel } = resolved;
10028
10141
  try {
10029
- const params = translateResponsesRequest(
10142
+ let params = translateResponsesRequest(
10030
10143
  body,
10031
10144
  route.npm,
10032
10145
  {
@@ -10037,6 +10150,13 @@ async function startCodexProxy(routes, options = {}) {
10037
10150
  interleavedReasoningField: route.interleavedReasoningField
10038
10151
  }
10039
10152
  );
10153
+ if (route.contextWindow && route.contextWindow > 0) {
10154
+ const before = params.messages.length;
10155
+ params = trimToContextLimit(params, route.contextWindow);
10156
+ if (debug && params.messages.length < before) {
10157
+ log19(`context trim: model=${route.modelId} window=${route.contextWindow} kept=${params.messages.length}/${before} messages`);
10158
+ }
10159
+ }
10040
10160
  if (debug) {
10041
10161
  const effort = body.reasoning?.effort;
10042
10162
  log19(`model=${route.modelId} effort=${effort ?? "(none)"} providerOptions=${JSON.stringify(params.providerOptions)}`);
@@ -10052,8 +10172,13 @@ async function startCodexProxy(routes, options = {}) {
10052
10172
  await streamResponsesResponse(languageModel, params, modelId, write);
10053
10173
  } catch (err) {
10054
10174
  const msg = formatUpstreamError(err);
10055
- logUpstreamError(err, route.modelId);
10056
- writeResponsesErrorStream(modelId, msg, write, upstreamHttpStatus(err, msg));
10175
+ const status = upstreamHttpStatus(err, msg);
10176
+ if (debug) log19(`sdk error: ${route.modelId}: ${msg}`);
10177
+ if (status === 429) {
10178
+ writeResponsesRateLimitStream(modelId, msg, write);
10179
+ } else {
10180
+ writeResponsesErrorStream(modelId, msg, write, status);
10181
+ }
10057
10182
  }
10058
10183
  res.end();
10059
10184
  } else {
@@ -10062,9 +10187,13 @@ async function startCodexProxy(routes, options = {}) {
10062
10187
  sendJson(res, 200, response);
10063
10188
  } catch (err) {
10064
10189
  const msg = formatUpstreamError(err);
10065
- logUpstreamError(err, route.modelId);
10066
10190
  const status = upstreamHttpStatus(err, msg);
10067
- sendJson(res, status, { error: { message: msg, type: status === 429 ? "rate_limit_error" : "api_error" } });
10191
+ if (debug) log19(`sdk error: ${route.modelId}: ${msg}`);
10192
+ if (status === 429) {
10193
+ sendJson(res, 200, responsesRateLimitBody(modelId, msg));
10194
+ } else {
10195
+ sendJson(res, status, { error: { message: msg, type: "api_error" } });
10196
+ }
10068
10197
  }
10069
10198
  }
10070
10199
  } catch (err) {
@@ -10169,7 +10298,8 @@ function buildCodexProxyRoutesForProvider(provider, apiKey, selectedModelId, age
10169
10298
  oauthAccountId: route.oauthAccountId,
10170
10299
  supportedParameters: route.supportedParameters,
10171
10300
  reasoning: route.reasoning,
10172
- interleavedReasoningField: route.interleavedReasoningField
10301
+ interleavedReasoningField: route.interleavedReasoningField,
10302
+ contextWindow: route.contextWindow
10173
10303
  };
10174
10304
  });
10175
10305
  }
@@ -10681,7 +10811,8 @@ function buildCodexProxyRoutesFromResolved(resolved, providersById) {
10681
10811
  upstreamModelId: route.upstreamModelId,
10682
10812
  providerId: route.providerId,
10683
10813
  authType: route.authType,
10684
- oauthAccountId: route.oauthAccountId
10814
+ oauthAccountId: route.oauthAccountId,
10815
+ contextWindow: route.contextWindow
10685
10816
  };
10686
10817
  }).filter((r) => r !== void 0);
10687
10818
  if (skippedOAuth.length > 0) {
@@ -12430,6 +12561,18 @@ function rootString(config, key) {
12430
12561
  const v = config[key];
12431
12562
  return { had: true, value: typeof v === "string" ? v : String(v ?? "") };
12432
12563
  }
12564
+ function rootNumber(config, key) {
12565
+ if (!(key in config)) return { had: false };
12566
+ const v = config[key];
12567
+ return { had: true, value: typeof v === "number" ? v : void 0 };
12568
+ }
12569
+ function applyRestoreNumber(config, key, had, value) {
12570
+ if (had && value !== void 0) {
12571
+ config[key] = value;
12572
+ } else {
12573
+ delete config[key];
12574
+ }
12575
+ }
12433
12576
  function readCodexConfigText(path = getCodexConfigPath()) {
12434
12577
  if (!existsSync14(path)) return "";
12435
12578
  return readFileSync11(path, "utf8");
@@ -12446,6 +12589,8 @@ function captureRestoreState(text5) {
12446
12589
  const modelCatalog = rootString(config, "model_catalog_json");
12447
12590
  const openAIBaseUrl = rootString(config, "openai_base_url");
12448
12591
  const reasoning = rootString(config, "model_reasoning_effort");
12592
+ const contextWindow = rootNumber(config, "model_context_window");
12593
+ const autoCompact = rootNumber(config, "model_auto_compact_token_limit");
12449
12594
  return {
12450
12595
  hadProfile: profile.had,
12451
12596
  profile: profile.value,
@@ -12458,7 +12603,11 @@ function captureRestoreState(text5) {
12458
12603
  hadOpenAIBaseUrl: openAIBaseUrl.had,
12459
12604
  openAIBaseUrl: openAIBaseUrl.value,
12460
12605
  hadModelReasoningEffort: reasoning.had,
12461
- modelReasoningEffort: reasoning.value
12606
+ modelReasoningEffort: reasoning.value,
12607
+ hadModelContextWindow: contextWindow.had,
12608
+ modelContextWindow: contextWindow.value,
12609
+ hadModelAutoCompactTokenLimit: autoCompact.had,
12610
+ modelAutoCompactTokenLimit: autoCompact.value
12462
12611
  };
12463
12612
  }
12464
12613
  function isAppManagedConfig(text5) {
@@ -12477,6 +12626,16 @@ function mergeAppConfig(existing, spec) {
12477
12626
  out.model_provider = patch.model_provider;
12478
12627
  out.openai_base_url = patch.openai_base_url;
12479
12628
  out.model_catalog_json = patch.model_catalog_json;
12629
+ if (patch.model_context_window !== void 0) {
12630
+ out.model_context_window = patch.model_context_window;
12631
+ } else {
12632
+ delete out.model_context_window;
12633
+ }
12634
+ if (patch.model_auto_compact_token_limit !== void 0) {
12635
+ out.model_auto_compact_token_limit = patch.model_auto_compact_token_limit;
12636
+ } else {
12637
+ delete out.model_auto_compact_token_limit;
12638
+ }
12480
12639
  const providers = asRecord(out.model_providers);
12481
12640
  delete providers[CODEX_APP_PROVIDER_ID];
12482
12641
  const profiles = asRecord(out.profiles);
@@ -12577,6 +12736,8 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
12577
12736
  applyRestoreKey(config, "openai_base_url", Boolean(state.hadOpenAIBaseUrl), state.openAIBaseUrl);
12578
12737
  }
12579
12738
  applyRestoreKey(config, "model_reasoning_effort", state.hadModelReasoningEffort, state.modelReasoningEffort);
12739
+ applyRestoreNumber(config, "model_context_window", state.hadModelContextWindow ?? false, state.modelContextWindow);
12740
+ applyRestoreNumber(config, "model_auto_compact_token_limit", state.hadModelAutoCompactTokenLimit ?? false, state.modelAutoCompactTokenLimit);
12580
12741
  const sidecar = getCodexAppSidecarProfilePath();
12581
12742
  if (existsSync14(sidecar)) {
12582
12743
  try {
@@ -12744,6 +12905,7 @@ function waitForShutdown2() {
12744
12905
  const cleanup = () => {
12745
12906
  process.removeListener("SIGINT", onSigint);
12746
12907
  process.removeListener("SIGTERM", onSigterm);
12908
+ process.removeListener("SIGHUP", onSighup);
12747
12909
  };
12748
12910
  const onSigint = () => {
12749
12911
  cleanup();
@@ -12753,8 +12915,13 @@ function waitForShutdown2() {
12753
12915
  cleanup();
12754
12916
  resolve("sigterm");
12755
12917
  };
12918
+ const onSighup = () => {
12919
+ cleanup();
12920
+ resolve("sighup");
12921
+ };
12756
12922
  process.once("SIGINT", onSigint);
12757
12923
  process.once("SIGTERM", onSigterm);
12924
+ process.once("SIGHUP", onSighup);
12758
12925
  });
12759
12926
  }
12760
12927
 
@@ -12968,6 +13135,21 @@ function codexAppInstallHint() {
12968
13135
  }
12969
13136
 
12970
13137
  // src/codex-app.ts
13138
+ async function waitForShutdownWithConfirm() {
13139
+ while (true) {
13140
+ const signal = await waitForShutdown2();
13141
+ if (signal !== "sigint") break;
13142
+ console.log("");
13143
+ const choice = await p17.select({
13144
+ message: "Close Codex Desktop and restore your Codex config?",
13145
+ options: [
13146
+ { value: "yes", label: "Yes, close Codex and restore config" },
13147
+ { value: "no", label: "No, keep session running" }
13148
+ ]
13149
+ });
13150
+ if (p17.isCancel(choice) || choice === "yes") break;
13151
+ }
13152
+ }
12971
13153
  function codexAppHelpText() {
12972
13154
  return `${pc15.bold("relay-ai codex-app")} \u2014 launch Codex desktop app with your registry providers
12973
13155
 
@@ -13068,7 +13250,8 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
13068
13250
  upstreamModelId: selectedEntry.upstream_id ?? selectedEntry.id,
13069
13251
  npm: VERTEX_ANTHROPIC_NPM,
13070
13252
  apiKey: "",
13071
- providerId: "vertex"
13253
+ providerId: "vertex",
13254
+ contextWindow: resolveContextWindow(selectedEntry.id)
13072
13255
  };
13073
13256
  if (configOnly) {
13074
13257
  const home = process.env["HOME"] ?? "";
@@ -13141,18 +13324,16 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
13141
13324
  restoreCommand: "relay-ai codex-app --restore"
13142
13325
  });
13143
13326
  codexAppOutro(selectedEntry.display_name);
13144
- await waitForShutdown2();
13327
+ await waitForShutdownWithConfirm();
13145
13328
  console.log("");
13329
+ if (isCodexAppRunning()) {
13330
+ p17.log.step("Stopping Codex Desktop...");
13331
+ quitCodexAppGracefully();
13332
+ }
13146
13333
  if (sessionActive) {
13147
13334
  restoreCodexAppOverlay();
13148
13335
  sessionActive = false;
13149
13336
  }
13150
- if (isCodexAppRunning()) {
13151
- const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
13152
- if (shouldClose && !p17.isCancel(shouldClose)) {
13153
- quitCodexAppGracefully();
13154
- }
13155
- }
13156
13337
  return 0;
13157
13338
  } finally {
13158
13339
  proxyHandle?.close();
@@ -13323,7 +13504,8 @@ async function runCodexAppCommand(args, opts = {}) {
13323
13504
  providerId: activeProvider.id,
13324
13505
  npm: "",
13325
13506
  upstreamModelId: "",
13326
- apiKey: ""
13507
+ apiKey: "",
13508
+ contextWindow: selectedModel.contextWindow
13327
13509
  } : appRoute;
13328
13510
  const specBase = { route: activeRoute, catalogPath };
13329
13511
  if (configOnly) {
@@ -13413,19 +13595,17 @@ async function runCodexAppCommand(args, opts = {}) {
13413
13595
  restoreCommand: "relay-ai codex-app --restore"
13414
13596
  });
13415
13597
  codexAppOutro(modelLabel);
13416
- await waitForShutdown2();
13598
+ await waitForShutdownWithConfirm();
13417
13599
  if (trace) printTraceLog(debugLogPath);
13418
13600
  console.log("");
13601
+ if (isCodexAppRunning()) {
13602
+ p17.log.step("Stopping Codex Desktop...");
13603
+ quitCodexAppGracefully();
13604
+ }
13419
13605
  if (sessionActive) {
13420
13606
  restoreCodexAppOverlay();
13421
13607
  sessionActive = false;
13422
13608
  }
13423
- if (isCodexAppRunning()) {
13424
- const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
13425
- if (shouldClose && !p17.isCancel(shouldClose)) {
13426
- quitCodexAppGracefully();
13427
- }
13428
- }
13429
13609
  return 0;
13430
13610
  } finally {
13431
13611
  proxyHandle?.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobbd/relay-ai",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },