@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.
- package/CHANGELOG.md +28 -0
- package/dist/cli.js +289 -96
- 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.
|
|
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
|
|
9119
|
+
const confirm9 = await p10.confirm({
|
|
9114
9120
|
message: `Remove ${provider.name} (${id})?`,
|
|
9115
9121
|
initialValue: false
|
|
9116
9122
|
});
|
|
9117
|
-
if (p10.isCancel(
|
|
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
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
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
|
|
9791
|
-
function
|
|
9792
|
-
|
|
9793
|
-
const
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
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
|
-
}
|
|
9932
|
+
} else if (typeof msg.content === "string") {
|
|
9933
|
+
chars += msg.content.length;
|
|
9806
9934
|
}
|
|
9807
9935
|
}
|
|
9808
|
-
|
|
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
|
|
9828
|
-
const
|
|
9829
|
-
if (
|
|
9830
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
10123
|
+
let resolved = resolveModel(routes, models, modelId);
|
|
10011
10124
|
if (!resolved) {
|
|
10012
|
-
|
|
10013
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10047
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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();
|