@jacobbd/relay-ai 0.3.1 → 0.3.3
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 +24 -0
- package/dist/cli.js +272 -92
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.3] - 2026-06-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Codex App: old sessions no longer show "Custom" as the model name** — relay-ai previously wrote its internal alias model ID (e.g. `go__glm-5.2`) into `config.toml`, which Codex baked into every session record. Reopening that conversation in native Codex showed "Custom" because the alias is unrecognized. relay-ai now writes `gpt-5.5` as the display model so sessions record a name Codex recognizes, enabling clean resume without errors.
|
|
8
|
+
|
|
9
|
+
## [0.3.2] - 2026-06-22
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
15
|
+
- **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.
|
|
16
|
+
|
|
17
|
+
- **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.
|
|
18
|
+
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
25
|
+
- **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.
|
|
26
|
+
|
|
3
27
|
## [0.3.1] - 2026-06-22
|
|
4
28
|
|
|
5
29
|
### 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.
|
|
38
|
+
version: "0.3.3",
|
|
39
39
|
publishConfig: {
|
|
40
40
|
access: "public"
|
|
41
41
|
},
|
|
@@ -822,6 +822,7 @@ function thinkingProviderOptions(npm) {
|
|
|
822
822
|
|
|
823
823
|
// src/codex/app-profile.ts
|
|
824
824
|
var CODEX_APP_PROVIDER_ID = "relay-ai-launch-codex-app";
|
|
825
|
+
var CODEX_APP_DISPLAY_MODEL = "gpt-5.5";
|
|
825
826
|
var PREVIEW_PROXY_PORT = 54321;
|
|
826
827
|
function codexAppModelSlug(rawModelId) {
|
|
827
828
|
return rawModelId.startsWith("models/") ? rawModelId.slice("models/".length) : rawModelId;
|
|
@@ -831,12 +832,16 @@ function parseCodexAppModelSlug(modelKey) {
|
|
|
831
832
|
return modelKey.startsWith(prefix) ? modelKey.slice(prefix.length) : modelKey;
|
|
832
833
|
}
|
|
833
834
|
function buildCodexAppRootConfig(spec) {
|
|
834
|
-
const
|
|
835
|
+
const ctxWindow = spec.route.contextWindow;
|
|
835
836
|
return {
|
|
836
|
-
model:
|
|
837
|
+
model: CODEX_APP_DISPLAY_MODEL,
|
|
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
|
|
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,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
|
-
|
|
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
|
-
|
|
10056
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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();
|