@jacobbd/relay-ai 0.3.0 → 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 +28 -0
  2. package/dist/cli.js +289 -96
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
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
+
21
+ ## [0.3.1] - 2026-06-22
22
+
23
+ ### Fixed
24
+
25
+ - **Codex App: background GPT model requests no longer crash your session** — The Codex desktop app has an internal agent subsystem that sends background requests using hardcoded model IDs (`gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`), even when you've configured a completely different model like GLM or DeepSeek. These requests were hitting the relay-ai proxy and getting 404 errors, which interrupted your chat session and showed up as confusing error states in the UI. The proxy now silently routes those background requests to your configured starting model instead. Your session keeps running. (Fixes [#8](https://github.com/jacob-bd/relay-ai/issues/8))
26
+
27
+ - **Codex App: `GET /v1/responses` polling no longer returns 404** — Codex polls this endpoint in the background for session state. The proxy only handled `POST /v1/responses` before, so every poll got a 404. Now it returns an empty list, which is all Codex actually needs.
28
+
29
+ - **`--trace` output was a false negative** — `relay-ai codex-app --trace` would print `(no errors found in debug log)` even when the proxy had been silently dropping dozens of model-not-found failures the whole session. Trace output now surfaces `resolveModel failed` and `resolveModel fallback` lines so you can actually see what's happening.
30
+
3
31
  ## [0.3.0] - 2026-06-21
4
32
 
5
33
  *Happy Father's Day!* 👨‍👦
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.0",
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
 
@@ -3640,7 +3645,7 @@ function printTraceLog(debugLogPath) {
3640
3645
  const raw = readFileSync6(debugLogPath, "utf8");
3641
3646
  const log19 = redactTraceLog(raw);
3642
3647
  const errorLines = log19.split("\n").filter(
3643
- (l) => l.includes("error") || l.includes("Error") || l.includes('"type":"error"') || l.includes("status")
3648
+ (l) => l.includes("error") || l.includes("Error") || l.includes('"type":"error"') || l.includes("status") || l.includes("resolveModel failed") || l.includes("resolveModel fallback")
3644
3649
  );
3645
3650
  console.log("\n" + pc2.bold(pc2.cyan("\u2500\u2500 Debug trace \u2500\u2500")));
3646
3651
  if (errorLines.length > 0) {
@@ -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,18 +10114,32 @@ 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
- const resolved = resolveModel(routes, models, modelId);
10123
+ let resolved = resolveModel(routes, models, modelId);
10011
10124
  if (!resolved) {
10012
- if (debug) {
10013
- log19(`resolveModel failed: requested="${modelId}" known=[${routes.map((r) => r.modelId).join(", ")}]`);
10125
+ const fallbackRoute = routes[0];
10126
+ const fallbackLm = fallbackRoute ? models.get(fallbackRoute.modelId) : void 0;
10127
+ if (fallbackRoute && fallbackLm) {
10128
+ if (debug) {
10129
+ log19(`resolveModel fallback: requested="${modelId}" \u2192 ${fallbackRoute.modelId}`);
10130
+ }
10131
+ resolved = { route: fallbackRoute, languageModel: fallbackLm };
10132
+ } else {
10133
+ if (debug) {
10134
+ log19(`resolveModel failed: requested="${modelId}" known=[${routes.map((r) => r.modelId).join(", ")}]`);
10135
+ }
10136
+ sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
10137
+ return;
10014
10138
  }
10015
- sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
10016
- return;
10017
10139
  }
10018
10140
  const { route, languageModel } = resolved;
10019
10141
  try {
10020
- const params = translateResponsesRequest(
10142
+ let params = translateResponsesRequest(
10021
10143
  body,
10022
10144
  route.npm,
10023
10145
  {
@@ -10028,6 +10150,13 @@ async function startCodexProxy(routes, options = {}) {
10028
10150
  interleavedReasoningField: route.interleavedReasoningField
10029
10151
  }
10030
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
+ }
10031
10160
  if (debug) {
10032
10161
  const effort = body.reasoning?.effort;
10033
10162
  log19(`model=${route.modelId} effort=${effort ?? "(none)"} providerOptions=${JSON.stringify(params.providerOptions)}`);
@@ -10043,8 +10172,13 @@ async function startCodexProxy(routes, options = {}) {
10043
10172
  await streamResponsesResponse(languageModel, params, modelId, write);
10044
10173
  } catch (err) {
10045
10174
  const msg = formatUpstreamError(err);
10046
- logUpstreamError(err, route.modelId);
10047
- 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
+ }
10048
10182
  }
10049
10183
  res.end();
10050
10184
  } else {
@@ -10053,9 +10187,13 @@ async function startCodexProxy(routes, options = {}) {
10053
10187
  sendJson(res, 200, response);
10054
10188
  } catch (err) {
10055
10189
  const msg = formatUpstreamError(err);
10056
- logUpstreamError(err, route.modelId);
10057
10190
  const status = upstreamHttpStatus(err, msg);
10058
- 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
+ }
10059
10197
  }
10060
10198
  }
10061
10199
  } catch (err) {
@@ -10065,6 +10203,10 @@ async function startCodexProxy(routes, options = {}) {
10065
10203
  }
10066
10204
  return;
10067
10205
  }
10206
+ if (req.method === "GET" && url === "/v1/responses") {
10207
+ sendJson(res, 200, { object: "list", data: [] });
10208
+ return;
10209
+ }
10068
10210
  sendJson(res, 404, { error: { message: "Not found", type: "invalid_request_error" } });
10069
10211
  });
10070
10212
  server.on("error", reject2);
@@ -10156,7 +10298,8 @@ function buildCodexProxyRoutesForProvider(provider, apiKey, selectedModelId, age
10156
10298
  oauthAccountId: route.oauthAccountId,
10157
10299
  supportedParameters: route.supportedParameters,
10158
10300
  reasoning: route.reasoning,
10159
- interleavedReasoningField: route.interleavedReasoningField
10301
+ interleavedReasoningField: route.interleavedReasoningField,
10302
+ contextWindow: route.contextWindow
10160
10303
  };
10161
10304
  });
10162
10305
  }
@@ -10668,7 +10811,8 @@ function buildCodexProxyRoutesFromResolved(resolved, providersById) {
10668
10811
  upstreamModelId: route.upstreamModelId,
10669
10812
  providerId: route.providerId,
10670
10813
  authType: route.authType,
10671
- oauthAccountId: route.oauthAccountId
10814
+ oauthAccountId: route.oauthAccountId,
10815
+ contextWindow: route.contextWindow
10672
10816
  };
10673
10817
  }).filter((r) => r !== void 0);
10674
10818
  if (skippedOAuth.length > 0) {
@@ -12417,6 +12561,18 @@ function rootString(config, key) {
12417
12561
  const v = config[key];
12418
12562
  return { had: true, value: typeof v === "string" ? v : String(v ?? "") };
12419
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
+ }
12420
12576
  function readCodexConfigText(path = getCodexConfigPath()) {
12421
12577
  if (!existsSync14(path)) return "";
12422
12578
  return readFileSync11(path, "utf8");
@@ -12433,6 +12589,8 @@ function captureRestoreState(text5) {
12433
12589
  const modelCatalog = rootString(config, "model_catalog_json");
12434
12590
  const openAIBaseUrl = rootString(config, "openai_base_url");
12435
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");
12436
12594
  return {
12437
12595
  hadProfile: profile.had,
12438
12596
  profile: profile.value,
@@ -12445,7 +12603,11 @@ function captureRestoreState(text5) {
12445
12603
  hadOpenAIBaseUrl: openAIBaseUrl.had,
12446
12604
  openAIBaseUrl: openAIBaseUrl.value,
12447
12605
  hadModelReasoningEffort: reasoning.had,
12448
- modelReasoningEffort: reasoning.value
12606
+ modelReasoningEffort: reasoning.value,
12607
+ hadModelContextWindow: contextWindow.had,
12608
+ modelContextWindow: contextWindow.value,
12609
+ hadModelAutoCompactTokenLimit: autoCompact.had,
12610
+ modelAutoCompactTokenLimit: autoCompact.value
12449
12611
  };
12450
12612
  }
12451
12613
  function isAppManagedConfig(text5) {
@@ -12464,6 +12626,16 @@ function mergeAppConfig(existing, spec) {
12464
12626
  out.model_provider = patch.model_provider;
12465
12627
  out.openai_base_url = patch.openai_base_url;
12466
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
+ }
12467
12639
  const providers = asRecord(out.model_providers);
12468
12640
  delete providers[CODEX_APP_PROVIDER_ID];
12469
12641
  const profiles = asRecord(out.profiles);
@@ -12564,6 +12736,8 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
12564
12736
  applyRestoreKey(config, "openai_base_url", Boolean(state.hadOpenAIBaseUrl), state.openAIBaseUrl);
12565
12737
  }
12566
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);
12567
12741
  const sidecar = getCodexAppSidecarProfilePath();
12568
12742
  if (existsSync14(sidecar)) {
12569
12743
  try {
@@ -12731,6 +12905,7 @@ function waitForShutdown2() {
12731
12905
  const cleanup = () => {
12732
12906
  process.removeListener("SIGINT", onSigint);
12733
12907
  process.removeListener("SIGTERM", onSigterm);
12908
+ process.removeListener("SIGHUP", onSighup);
12734
12909
  };
12735
12910
  const onSigint = () => {
12736
12911
  cleanup();
@@ -12740,8 +12915,13 @@ function waitForShutdown2() {
12740
12915
  cleanup();
12741
12916
  resolve("sigterm");
12742
12917
  };
12918
+ const onSighup = () => {
12919
+ cleanup();
12920
+ resolve("sighup");
12921
+ };
12743
12922
  process.once("SIGINT", onSigint);
12744
12923
  process.once("SIGTERM", onSigterm);
12924
+ process.once("SIGHUP", onSighup);
12745
12925
  });
12746
12926
  }
12747
12927
 
@@ -12955,6 +13135,21 @@ function codexAppInstallHint() {
12955
13135
  }
12956
13136
 
12957
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
+ }
12958
13153
  function codexAppHelpText() {
12959
13154
  return `${pc15.bold("relay-ai codex-app")} \u2014 launch Codex desktop app with your registry providers
12960
13155
 
@@ -13055,7 +13250,8 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
13055
13250
  upstreamModelId: selectedEntry.upstream_id ?? selectedEntry.id,
13056
13251
  npm: VERTEX_ANTHROPIC_NPM,
13057
13252
  apiKey: "",
13058
- providerId: "vertex"
13253
+ providerId: "vertex",
13254
+ contextWindow: resolveContextWindow(selectedEntry.id)
13059
13255
  };
13060
13256
  if (configOnly) {
13061
13257
  const home = process.env["HOME"] ?? "";
@@ -13128,18 +13324,16 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
13128
13324
  restoreCommand: "relay-ai codex-app --restore"
13129
13325
  });
13130
13326
  codexAppOutro(selectedEntry.display_name);
13131
- await waitForShutdown2();
13327
+ await waitForShutdownWithConfirm();
13132
13328
  console.log("");
13329
+ if (isCodexAppRunning()) {
13330
+ p17.log.step("Stopping Codex Desktop...");
13331
+ quitCodexAppGracefully();
13332
+ }
13133
13333
  if (sessionActive) {
13134
13334
  restoreCodexAppOverlay();
13135
13335
  sessionActive = false;
13136
13336
  }
13137
- if (isCodexAppRunning()) {
13138
- const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
13139
- if (shouldClose && !p17.isCancel(shouldClose)) {
13140
- quitCodexAppGracefully();
13141
- }
13142
- }
13143
13337
  return 0;
13144
13338
  } finally {
13145
13339
  proxyHandle?.close();
@@ -13310,7 +13504,8 @@ async function runCodexAppCommand(args, opts = {}) {
13310
13504
  providerId: activeProvider.id,
13311
13505
  npm: "",
13312
13506
  upstreamModelId: "",
13313
- apiKey: ""
13507
+ apiKey: "",
13508
+ contextWindow: selectedModel.contextWindow
13314
13509
  } : appRoute;
13315
13510
  const specBase = { route: activeRoute, catalogPath };
13316
13511
  if (configOnly) {
@@ -13400,19 +13595,17 @@ async function runCodexAppCommand(args, opts = {}) {
13400
13595
  restoreCommand: "relay-ai codex-app --restore"
13401
13596
  });
13402
13597
  codexAppOutro(modelLabel);
13403
- await waitForShutdown2();
13598
+ await waitForShutdownWithConfirm();
13404
13599
  if (trace) printTraceLog(debugLogPath);
13405
13600
  console.log("");
13601
+ if (isCodexAppRunning()) {
13602
+ p17.log.step("Stopping Codex Desktop...");
13603
+ quitCodexAppGracefully();
13604
+ }
13406
13605
  if (sessionActive) {
13407
13606
  restoreCodexAppOverlay();
13408
13607
  sessionActive = false;
13409
13608
  }
13410
- if (isCodexAppRunning()) {
13411
- const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
13412
- if (shouldClose && !p17.isCancel(shouldClose)) {
13413
- quitCodexAppGracefully();
13414
- }
13415
- }
13416
13609
  return 0;
13417
13610
  } finally {
13418
13611
  proxyHandle?.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobbd/relay-ai",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },