codex-copilot-dx 0.6.3 → 0.6.5
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/README.md +22 -20
- package/bin/codex-copilot-dx.mjs +8 -1
- package/package.json +1 -1
- package/src/adapter.mjs +19 -7
- package/src/anthropic-handler.mjs +8 -4
- package/src/auto-review-model.mjs +1 -1
- package/src/cli-options.mjs +1 -1
- package/src/http-transport.mjs +210 -26
- package/src/launcher.mjs +2 -3
- package/src/legacy-command-warning.mjs +37 -0
- package/src/pm-studio-relay.mjs +8 -2
- package/src/response-history.mjs +11 -2
- package/src/responses-bridge.mjs +28 -13
- package/src/responses-handler.mjs +100 -18
- package/src/responses-image-pressure.mjs +19 -2
- package/src/responses-proxy.mjs +2 -2
- package/src/responses-request.mjs +8 -0
- package/src/runtime-config.mjs +2 -0
package/README.md
CHANGED
|
@@ -31,7 +31,8 @@ ccdx
|
|
|
31
31
|
|
|
32
32
|
`ccdx` is the only documented command and primary launcher. The old
|
|
33
33
|
`codex-copilot-dx` executable remains temporarily as a compatibility shim: it
|
|
34
|
-
prints a deprecation warning
|
|
34
|
+
prints a deprecation warning at most once every 7 days in interactive terminals,
|
|
35
|
+
runs `ccdx`, and remains silent in scripts. It will be removed in a future
|
|
35
36
|
breaking release. For a one-off run without a global install, use
|
|
36
37
|
`npm exec --yes --package=codex-copilot-dx@latest -- ccdx`.
|
|
37
38
|
Run `ccdx <command> --help` (including `auth login claude --help` and
|
|
@@ -110,7 +111,7 @@ active account or profile. GPT requests continue to use the bearer supplied by
|
|
|
110
111
|
the current PM profile; exact enabled Anthropic model IDs use the isolated
|
|
111
112
|
CCDX Claude profile. Neither credential is copied into the other application.
|
|
112
113
|
|
|
113
|
-
The
|
|
114
|
+
The current patch recipe supports only an exact, unmodified PM Studio 2.9.7 bundle on
|
|
114
115
|
macOS. Authorize the isolated Claude profile first, quit PM Studio and its
|
|
115
116
|
updater, then run the one-time setup command:
|
|
116
117
|
|
|
@@ -126,7 +127,7 @@ and relay without changing files:
|
|
|
126
127
|
ccdx pms status
|
|
127
128
|
```
|
|
128
129
|
|
|
129
|
-
`pm-studio status` and `pm-studio setup` are equivalent long-form aliases.
|
|
130
|
+
`ccdx pm-studio status` and `ccdx pm-studio setup` are equivalent long-form aliases.
|
|
130
131
|
|
|
131
132
|
`ccdx pms setup` performs only local preflight, backup, staging, integrity,
|
|
132
133
|
signing, and replacement work. It never starts Device Flow, the adapter,
|
|
@@ -186,13 +187,13 @@ To change the model used by Codex Auto-review, run:
|
|
|
186
187
|
ccdx auto-review-model
|
|
187
188
|
```
|
|
188
189
|
|
|
189
|
-
The selector
|
|
190
|
-
model
|
|
190
|
+
The selector first queries the running adapter, including its last-known-good
|
|
191
|
+
model list when live refresh is unavailable, then falls back to the non-expired
|
|
192
|
+
local model cache. It offers only enabled models that advertise a Responses endpoint.
|
|
191
193
|
It saves the selection in `~/.config/codex-copilot-dx/config.json` (or under
|
|
192
194
|
`XDG_CONFIG_HOME` when set); choosing `gpt-5.5` clears the override and restores
|
|
193
|
-
the package default. A running
|
|
194
|
-
|
|
195
|
-
be restarted.
|
|
195
|
+
the package default. A running adapter reads the setting on the next Auto-review
|
|
196
|
+
request, so it does not need to be restarted.
|
|
196
197
|
|
|
197
198
|
To update the globally installed package, choose a source interactively:
|
|
198
199
|
|
|
@@ -212,26 +213,26 @@ ccdx update github
|
|
|
212
213
|
The npm source installs `codex-copilot-dx@latest` through the registry already
|
|
213
214
|
configured for npm, including a company mirror. The GitHub source installs the
|
|
214
215
|
latest commit from `DaleXiao/codex-copilot-dx` `main` and opts in to Git fetching
|
|
215
|
-
for that command
|
|
216
|
-
|
|
216
|
+
for that command. It does not change npm's persistent `allow-git` setting. Both
|
|
217
|
+
paths use npm's global installer without shell
|
|
217
218
|
interpolation. A currently running adapter keeps its loaded version until it is
|
|
218
219
|
stopped and started again.
|
|
219
220
|
If a configured npm mirror has not synchronized the current release yet, use
|
|
220
221
|
the GitHub source to install the current `main` revision.
|
|
221
222
|
|
|
222
|
-
On
|
|
223
|
-
1.
|
|
224
|
-
2.
|
|
225
|
-
3.
|
|
223
|
+
On a normal launch, it will:
|
|
224
|
+
1. Print the local package version and check for a newer npm release in the background
|
|
225
|
+
2. Reuse a compatible running adapter when available; otherwise authenticate with GitHub if needed, after first trying compatible local Copilot token sources
|
|
226
|
+
3. When starting a new adapter, refresh Copilot model metadata and listen on loopback (`127.0.0.1:2026`)
|
|
226
227
|
4. Configure Codex (`~/.codex/config.toml`) to use the adapter, including stale shell env base URLs if present
|
|
227
228
|
5. Configure Claude Code (`~/.claude/settings.json`) to use the adapter; it creates the file when missing, otherwise backs up `settings.json.bak` before updating the local API env keys
|
|
228
|
-
6.
|
|
229
|
+
6. On macOS, attempt to launch Codex or ChatGPT unless `CCDX_AUTO_LAUNCH` disables it
|
|
229
230
|
|
|
230
231
|
Claude Code picks up the new `ANTHROPIC_BASE_URL` on its next launch.
|
|
231
232
|
|
|
232
|
-
If
|
|
233
|
+
If a compatible CCDX adapter is already running on the configured host and port, a second launch reuses it instead of starting another proxy. The second launch still refreshes Codex and Claude Code config, then exits.
|
|
233
234
|
|
|
234
|
-
The running adapter reports its package and protocol versions. After upgrading
|
|
235
|
+
The running adapter reports its package and protocol versions. After upgrading the package, stop the old process before starting the new version; the new CLI refuses to silently reuse an incompatible adapter.
|
|
235
236
|
|
|
236
237
|
Do not set Claude Code by manually exporting `ANTHROPIC_BASE_URL` or `ANTHROPIC_AUTH_TOKEN` in your shell. Let `ccdx` write the local config files instead. If you previously exported those variables, remove them from shell startup files and restart the terminal before launching Claude Code.
|
|
237
238
|
|
|
@@ -330,12 +331,13 @@ Environment variables:
|
|
|
330
331
|
| `CCDX_ALLOW_LAN` | unset | Set to `1` to allow non-loopback `ADAPTER_HOST` values such as `0.0.0.0`; exposes your Copilot-backed adapter beyond this machine |
|
|
331
332
|
| `CCDX_MAX_BODY_BYTES` | `67108864` | Maximum compressed/raw request body size |
|
|
332
333
|
| `CCDX_MAX_DECODED_BODY_BYTES` | `134217728` | Maximum decoded request body size after decompression |
|
|
333
|
-
| `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` |
|
|
334
|
+
| `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` | Per-pool byte budget for raw bodies, decoded bodies, and materialized response history; a larger item runs exclusively in its pool |
|
|
334
335
|
| `CCDX_MAX_QUEUED_REQUESTS` | `16` | Maximum body requests waiting for the shared byte budget |
|
|
335
336
|
| `CCDX_REQUEST_QUEUE_TIMEOUT_MS` | `120000` | Maximum wait for request-body admission before returning `503` |
|
|
337
|
+
| `CCDX_REQUEST_BODY_TIMEOUT_MS` | `120000` | Maximum time to receive and decode one request body before returning `408` |
|
|
336
338
|
| `CCDX_MAX_UPSTREAM_BODY_BYTES` | `31457280` | Strict maximum forwarded Responses body size; larger payloads are adapted locally or rejected with `413` |
|
|
337
339
|
| `CCDX_MAX_SSE_BUFFER_BYTES` | `8388608` | Maximum buffered bytes for one unterminated upstream SSE line/event |
|
|
338
|
-
| `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` |
|
|
340
|
+
| `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Per-phase timeout for Responses preparation and non-streaming upstream Copilot requests |
|
|
339
341
|
| `CCDX_STREAM_HANDSHAKE_TIMEOUT_MS` | `120000` | Timeout while waiting for upstream streaming response headers |
|
|
340
342
|
| `CCDX_STREAM_IDLE_TIMEOUT_MS` | `120000` | Maximum idle time between upstream streaming body chunks |
|
|
341
343
|
| `CCDX_UPSTREAM_RETRIES` | `2` | Retries for safe requests and clearly pre-connect POST failures; capped at `5` |
|
|
@@ -396,7 +398,7 @@ The final serialized UTF-8 request body is measured before forwarding. Above the
|
|
|
396
398
|
|
|
397
399
|
Completed image transforms are reused from a byte-bounded process-local LRU cache, and concurrent requests for the same transform share one encode. The cache key includes source content, MIME type, model-aware dimensions, WebP quality, and encoder settings; failures are not cached and request cancellation remains isolated.
|
|
398
400
|
|
|
399
|
-
Before image encoding, CCDX leaves ordinary visual history byte-for-byte unchanged while it has at most 24 historical images and the expanded body is at most 18 MiB. Above either threshold, the temporary upstream view keeps every current-turn image and at most 16 recent historical images, targeting 16 MiB. If that history tree then times out during local preparation or upstream
|
|
401
|
+
Before image encoding, CCDX leaves ordinary visual history byte-for-byte unchanged while it has at most 24 historical images and the expanded body is at most 18 MiB. Above either threshold, the temporary upstream view keeps every current-turn image and at most 16 recent historical images, targeting 16 MiB. If that history tree then times out during local preparation, a streaming handshake, or a non-streaming upstream request, or if the upstream returns HTTP 408, the next retry automatically uses an 8-image, 10 MiB recovery window for ten minutes. Two successful requests or a successful compaction clear recovery mode. CCDX never transparently repeats the ambiguous timed-out POST, and no local history is deleted; retrying does not require restarting the service.
|
|
400
402
|
|
|
401
403
|
When expanded Responses history exceeds Copilot's 50-image request limit, the adapter removes older duplicate image occurrences first, then keeps the 50 most recent images. This applies only to the forwarded request: local history remains complete, current images are preferred over older history, and requests at or below the limit are unchanged.
|
|
402
404
|
|
package/bin/codex-copilot-dx.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
if (process.stderr.isTTY === true) {
|
|
4
|
+
const {
|
|
5
|
+
LEGACY_COMMAND_WARNING,
|
|
6
|
+
shouldShowLegacyCommandWarning,
|
|
7
|
+
} = await import("../src/legacy-command-warning.mjs");
|
|
8
|
+
if (shouldShowLegacyCommandWarning({ interactive: true })) console.warn(LEGACY_COMMAND_WARNING);
|
|
9
|
+
}
|
|
3
10
|
await import("./cli.mjs");
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -136,6 +136,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
136
136
|
const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, ADAPTER_RUNTIME_CONFIG.upstreamTimeoutMs);
|
|
137
137
|
const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamHandshakeTimeoutMs);
|
|
138
138
|
const streamIdleTimeoutMs = parsePositiveInteger(options.streamIdleTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamIdleTimeoutMs);
|
|
139
|
+
const requestBodyTimeoutMs = parsePositiveInteger(options.requestBodyTimeoutMs, ADAPTER_RUNTIME_CONFIG.requestBodyTimeoutMs);
|
|
139
140
|
const acquireRequest = options.acquireRequest || createRequestAdmission();
|
|
140
141
|
const requestMetrics = options.requestMetrics || createRequestMetrics();
|
|
141
142
|
const streamPerformanceMetrics = options.streamPerformanceMetrics || createStreamPerformanceMetrics();
|
|
@@ -150,9 +151,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
150
151
|
chatCompletionsFn: codexChatCompletionsFn,
|
|
151
152
|
getCachedModelEndpointsFn,
|
|
152
153
|
imagePressure,
|
|
154
|
+
now: options.now,
|
|
153
155
|
openAIModelEnv,
|
|
154
156
|
responsesPayloadOptions: options.responsesPayloadOptions,
|
|
155
157
|
responsesFn,
|
|
158
|
+
requestBodyTimeoutMs,
|
|
156
159
|
streamHandshakeTimeoutMs,
|
|
157
160
|
streamIdleTimeoutMs,
|
|
158
161
|
upstreamTimeoutMs,
|
|
@@ -161,18 +164,21 @@ export function createAdapterHandler(options = {}) {
|
|
|
161
164
|
acquireRequest,
|
|
162
165
|
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
163
166
|
imagePressure,
|
|
167
|
+
now: options.now,
|
|
164
168
|
openAIModelEnv,
|
|
169
|
+
requestBodyTimeoutMs,
|
|
165
170
|
responsesCompactFn,
|
|
166
171
|
streamHandshakeTimeoutMs,
|
|
167
172
|
streamIdleTimeoutMs,
|
|
168
173
|
upstreamTimeoutMs,
|
|
169
174
|
});
|
|
170
|
-
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest });
|
|
175
|
+
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest, requestBodyTimeoutMs });
|
|
171
176
|
const anthropicMessagesHandler = createAnthropicMessagesHandler({
|
|
172
177
|
acquireRequest,
|
|
173
178
|
chatCompletionsFn: claudeChatCompletionsFn,
|
|
174
179
|
environment: process.env,
|
|
175
180
|
modelOptions: claudeDesktopModelOptions,
|
|
181
|
+
requestBodyTimeoutMs,
|
|
176
182
|
streamHandshakeTimeoutMs,
|
|
177
183
|
streamIdleTimeoutMs,
|
|
178
184
|
upstreamTimeoutMs,
|
|
@@ -184,6 +190,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
184
190
|
claudeProfile: options.claudeProfile,
|
|
185
191
|
claudeModelRegistry,
|
|
186
192
|
fetchImpl: options.pmStudioFetchImpl,
|
|
193
|
+
requestBodyTimeoutMs,
|
|
187
194
|
streamHandshakeTimeoutMs,
|
|
188
195
|
streamIdleTimeoutMs,
|
|
189
196
|
upstreamTimeoutMs,
|
|
@@ -284,13 +291,20 @@ export function createAdapterHandler(options = {}) {
|
|
|
284
291
|
};
|
|
285
292
|
|
|
286
293
|
return (req, res) => {
|
|
287
|
-
const pathname = requestPath(req.url);
|
|
288
294
|
const requestId = createRequestId();
|
|
289
295
|
if (typeof res.setHeader === "function") {
|
|
290
296
|
res.setHeader("X-Request-Id", requestId);
|
|
291
297
|
} else if (res.headers && typeof res.headers === "object") {
|
|
292
298
|
res.headers["X-Request-Id"] = requestId;
|
|
293
299
|
}
|
|
300
|
+
let pathname;
|
|
301
|
+
try {
|
|
302
|
+
pathname = requestPath(req.url);
|
|
303
|
+
} catch {
|
|
304
|
+
res.writeHead(400, { "Content-Type": "application/json", Connection: "close" });
|
|
305
|
+
res.end(JSON.stringify({ error: "Invalid request target" }));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
294
308
|
|
|
295
309
|
const trackRequest = pathname !== ADAPTER_HEALTH_PATH && pathname !== ADAPTER_STATUS_PATH;
|
|
296
310
|
const routeName = classifyAdapterRoute(req.method, pathname);
|
|
@@ -352,11 +366,9 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
|
|
|
352
366
|
server.once("close", cleanupTerminalActivity);
|
|
353
367
|
|
|
354
368
|
server.on("upgrade", (req, socket) => {
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
// 5-attempt reconnect storm. Refuse the upgrade so Codex falls back to
|
|
359
|
-
// plain HTTP SSE, which this adapter handles correctly.
|
|
369
|
+
// The responses_websockets upgrade expects a server-push protocol that this
|
|
370
|
+
// adapter does not implement. Refuse it so the client can fall back to the
|
|
371
|
+
// supported HTTP SSE transport instead of waiting on an unusable socket.
|
|
360
372
|
socket.write("HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
361
373
|
socket.destroy();
|
|
362
374
|
});
|
|
@@ -21,20 +21,22 @@ import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
|
21
21
|
import { recordAnthropicUsage } from "./usage.mjs";
|
|
22
22
|
|
|
23
23
|
export function createAnthropicCountTokensHandler(options) {
|
|
24
|
-
const { acquireRequest } = options;
|
|
24
|
+
const { acquireRequest, requestBodyTimeoutMs } = options;
|
|
25
25
|
|
|
26
26
|
return async function handleAnthropicCountTokens(req, res) {
|
|
27
27
|
const abort = createRequestAbort(req, res);
|
|
28
28
|
let releaseRequest = () => {};
|
|
29
29
|
try {
|
|
30
30
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
31
|
-
|
|
31
|
+
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
32
|
+
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
33
|
+
abort.clearTimeout();
|
|
32
34
|
const result = await countTokens(parsed);
|
|
33
35
|
releaseRequest();
|
|
34
36
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
35
37
|
res.end(JSON.stringify(result));
|
|
36
38
|
} catch (error) {
|
|
37
|
-
sendJsonError(res, error);
|
|
39
|
+
sendJsonError(res, error, abort.reason === "request_body_timeout" ? 408 : 400);
|
|
38
40
|
} finally {
|
|
39
41
|
releaseRequest();
|
|
40
42
|
abort.cleanup();
|
|
@@ -48,6 +50,7 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
48
50
|
chatCompletionsFn,
|
|
49
51
|
environment,
|
|
50
52
|
modelOptions,
|
|
53
|
+
requestBodyTimeoutMs,
|
|
51
54
|
streamHandshakeTimeoutMs,
|
|
52
55
|
streamIdleTimeoutMs,
|
|
53
56
|
upstreamTimeoutMs,
|
|
@@ -58,7 +61,8 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
58
61
|
let releaseRequest = () => {};
|
|
59
62
|
try {
|
|
60
63
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
61
|
-
|
|
64
|
+
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
65
|
+
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
62
66
|
abort.setTimeout(
|
|
63
67
|
parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
64
68
|
parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
|
|
@@ -135,7 +135,7 @@ export async function runAutoReviewModelCommand({
|
|
|
135
135
|
if (String(env.CCDX_AUTO_REVIEW_MODEL || "").trim()) {
|
|
136
136
|
output.write(`CCDX_AUTO_REVIEW_MODEL=${current.model} remains the effective override until it is unset.\n`);
|
|
137
137
|
} else {
|
|
138
|
-
output.write("The running
|
|
138
|
+
output.write("The running adapter will use this model on the next Auto Review request.\n");
|
|
139
139
|
}
|
|
140
140
|
return { ...result, cancelled: false, model: selectedModel };
|
|
141
141
|
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -258,7 +258,7 @@ export function cliCommandName() {
|
|
|
258
258
|
|
|
259
259
|
function topicHelp(name, topic) {
|
|
260
260
|
const sections = {
|
|
261
|
-
start: `Usage:\n ${name} [start] [--configure-claude-app] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex and Claude Code configuration, and
|
|
261
|
+
start: `Usage:\n ${name} [start] [--configure-claude-app] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex and Claude Code configuration, and attempts to open Codex or ChatGPT when auto-launch is enabled and supported.\n--configure-claude-app also creates or updates the managed Claude App gateway profile.`,
|
|
262
262
|
auth: `Usage:\n ${name} auth status [--online]\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nShows the two account profiles or configures the isolated Claude account. Device login starts only when no reusable local Copilot credential is available or --reauth is used.`,
|
|
263
263
|
"auth status": `Usage:\n ${name} auth status [--online]\n\nShows account routing without exposing credentials. --online also verifies both configured Copilot entitlements and model catalogs.`,
|
|
264
264
|
"auth login": `Usage:\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nReuses a compatible local Copilot credential when possible. --reauth forces GitHub Device Flow.`,
|
package/src/http-transport.mjs
CHANGED
|
@@ -47,6 +47,83 @@ function admissionAbortError(signal) {
|
|
|
47
47
|
return error;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, label }) {
|
|
51
|
+
const queue = [];
|
|
52
|
+
let activeWeight = 0;
|
|
53
|
+
let activeRequests = 0;
|
|
54
|
+
|
|
55
|
+
const activate = (entry) => {
|
|
56
|
+
entry.cleanup();
|
|
57
|
+
activeWeight += entry.weight;
|
|
58
|
+
activeRequests += 1;
|
|
59
|
+
let released = false;
|
|
60
|
+
entry.resolve(() => {
|
|
61
|
+
if (released) return;
|
|
62
|
+
released = true;
|
|
63
|
+
activeWeight = Math.max(0, activeWeight - entry.weight);
|
|
64
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
65
|
+
drain();
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const drain = () => {
|
|
70
|
+
for (let index = 0; index < queue.length;) {
|
|
71
|
+
const entry = queue[index];
|
|
72
|
+
if (activeWeight + entry.weight > maxWeight) {
|
|
73
|
+
index += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
queue.splice(index, 1);
|
|
77
|
+
activate(entry);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const acquire = (requestedWeight, { signal } = {}) => {
|
|
82
|
+
if (signal?.aborted) return Promise.reject(admissionAbortError(signal));
|
|
83
|
+
const parsedWeight = Number.isFinite(requestedWeight) && requestedWeight > 0
|
|
84
|
+
? Math.ceil(requestedWeight)
|
|
85
|
+
: maxWeight;
|
|
86
|
+
const weight = Math.max(1, Math.min(parsedWeight, maxWeight));
|
|
87
|
+
if (queue.length >= maxQueued && activeWeight + weight > maxWeight) {
|
|
88
|
+
return Promise.reject(httpError(`${label} queue is full (${maxQueued} waiting)`, 503));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
let timer;
|
|
93
|
+
const entry = {
|
|
94
|
+
weight,
|
|
95
|
+
resolve,
|
|
96
|
+
cleanup: () => {
|
|
97
|
+
if (timer) clearTimeout(timer);
|
|
98
|
+
signal?.removeEventListener("abort", onAbort);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const remove = () => {
|
|
102
|
+
const index = queue.indexOf(entry);
|
|
103
|
+
if (index >= 0) queue.splice(index, 1);
|
|
104
|
+
};
|
|
105
|
+
const fail = (error) => {
|
|
106
|
+
entry.cleanup();
|
|
107
|
+
remove();
|
|
108
|
+
reject(error);
|
|
109
|
+
drain();
|
|
110
|
+
};
|
|
111
|
+
const onAbort = () => fail(admissionAbortError(signal));
|
|
112
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
113
|
+
timer = setTimeout(() => {
|
|
114
|
+
fail(httpError(`${label} admission timed out after ${waitTimeoutMs}ms`, 503));
|
|
115
|
+
}, waitTimeoutMs);
|
|
116
|
+
timer.unref?.();
|
|
117
|
+
queue.push(entry);
|
|
118
|
+
if (signal?.aborted) onAbort();
|
|
119
|
+
else drain();
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
acquire.stats = () => ({ activeWeight, activeRequests, queued: queue.length, maxWeight });
|
|
124
|
+
return acquire;
|
|
125
|
+
}
|
|
126
|
+
|
|
50
127
|
export function createRequestAdmission({
|
|
51
128
|
maxBytes = MAX_INFLIGHT_BODY_BYTES,
|
|
52
129
|
maxQueued = MAX_QUEUED_REQUESTS,
|
|
@@ -55,6 +132,24 @@ export function createRequestAdmission({
|
|
|
55
132
|
const byteLimit = parsePositiveInteger(maxBytes, MAX_INFLIGHT_BODY_BYTES);
|
|
56
133
|
const queueLimit = parsePositiveInteger(maxQueued, MAX_QUEUED_REQUESTS);
|
|
57
134
|
const timeoutMs = parsePositiveInteger(waitTimeoutMs, REQUEST_QUEUE_TIMEOUT_MS);
|
|
135
|
+
const acquireDecompression = createSupplementalAdmission({
|
|
136
|
+
maxWeight: 1,
|
|
137
|
+
maxQueued: queueLimit,
|
|
138
|
+
waitTimeoutMs: timeoutMs,
|
|
139
|
+
label: "Request decompression",
|
|
140
|
+
});
|
|
141
|
+
const acquireDecodedBody = createSupplementalAdmission({
|
|
142
|
+
maxWeight: byteLimit,
|
|
143
|
+
maxQueued: queueLimit,
|
|
144
|
+
waitTimeoutMs: timeoutMs,
|
|
145
|
+
label: "Decoded request body",
|
|
146
|
+
});
|
|
147
|
+
const acquireResponseHistory = createSupplementalAdmission({
|
|
148
|
+
maxWeight: byteLimit,
|
|
149
|
+
maxQueued: queueLimit,
|
|
150
|
+
waitTimeoutMs: timeoutMs,
|
|
151
|
+
label: "Response history",
|
|
152
|
+
});
|
|
58
153
|
const queue = [];
|
|
59
154
|
let activeBytes = 0;
|
|
60
155
|
let activeRequests = 0;
|
|
@@ -83,13 +178,28 @@ export function createRequestAdmission({
|
|
|
83
178
|
counters.waitMsTotal += waitMs;
|
|
84
179
|
counters.waitMsMax = Math.max(counters.waitMsMax, waitMs);
|
|
85
180
|
let released = false;
|
|
86
|
-
|
|
181
|
+
const supplementalReleases = new Set();
|
|
182
|
+
const release = () => {
|
|
87
183
|
if (released) return;
|
|
88
184
|
released = true;
|
|
185
|
+
for (const releaseSupplemental of supplementalReleases) releaseSupplemental();
|
|
186
|
+
supplementalReleases.clear();
|
|
89
187
|
activeBytes = Math.max(0, activeBytes - entry.weight);
|
|
90
188
|
activeRequests = Math.max(0, activeRequests - 1);
|
|
91
189
|
drain();
|
|
92
|
-
}
|
|
190
|
+
};
|
|
191
|
+
const reserve = async (gate, weight, options) => {
|
|
192
|
+
const releaseSupplemental = await gate(weight, options);
|
|
193
|
+
if (released) {
|
|
194
|
+
releaseSupplemental();
|
|
195
|
+
throw admissionAbortError(options?.signal);
|
|
196
|
+
}
|
|
197
|
+
supplementalReleases.add(releaseSupplemental);
|
|
198
|
+
};
|
|
199
|
+
release.acquireDecompression = (options) => acquireDecompression(1, options);
|
|
200
|
+
release.reserveDecodedBody = (bytes, options) => reserve(acquireDecodedBody, bytes, options);
|
|
201
|
+
release.reserveResponseHistory = (bytes, options) => reserve(acquireResponseHistory, bytes, options);
|
|
202
|
+
entry.resolve(release);
|
|
93
203
|
};
|
|
94
204
|
|
|
95
205
|
const drain = () => {
|
|
@@ -173,11 +283,64 @@ export function createRequestAdmission({
|
|
|
173
283
|
? Number((counters.waitMsTotal / counters.activated).toFixed(1))
|
|
174
284
|
: 0,
|
|
175
285
|
waitMsMax: counters.waitMsMax,
|
|
286
|
+
decompressionsActive: acquireDecompression.stats().activeRequests,
|
|
287
|
+
decompressionsQueued: acquireDecompression.stats().queued,
|
|
288
|
+
decodedBodyBytes: acquireDecodedBody.stats().activeWeight,
|
|
289
|
+
decodedBodiesActive: acquireDecodedBody.stats().activeRequests,
|
|
290
|
+
decodedBodiesQueued: acquireDecodedBody.stats().queued,
|
|
291
|
+
responseHistoryBytes: acquireResponseHistory.stats().activeWeight,
|
|
292
|
+
responseHistoriesActive: acquireResponseHistory.stats().activeRequests,
|
|
293
|
+
responseHistoriesQueued: acquireResponseHistory.stats().queued,
|
|
176
294
|
});
|
|
177
295
|
return acquire;
|
|
178
296
|
}
|
|
179
297
|
|
|
180
|
-
|
|
298
|
+
function consumeRequestChunks(req, { signal, onChunk }) {
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
let settled = false;
|
|
301
|
+
const cleanup = () => {
|
|
302
|
+
req.off("data", handleData);
|
|
303
|
+
req.off("end", handleEnd);
|
|
304
|
+
req.off("error", handleError);
|
|
305
|
+
req.off("aborted", handleAborted);
|
|
306
|
+
signal?.removeEventListener("abort", handleSignalAbort);
|
|
307
|
+
};
|
|
308
|
+
const finish = (error) => {
|
|
309
|
+
if (settled) return;
|
|
310
|
+
settled = true;
|
|
311
|
+
cleanup();
|
|
312
|
+
if (error) reject(error);
|
|
313
|
+
else resolve();
|
|
314
|
+
};
|
|
315
|
+
const stopReading = (error) => {
|
|
316
|
+
req.pause?.();
|
|
317
|
+
finish(error);
|
|
318
|
+
};
|
|
319
|
+
const handleData = (chunk) => {
|
|
320
|
+
try {
|
|
321
|
+
onChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
322
|
+
} catch (error) {
|
|
323
|
+
stopReading(error);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const handleEnd = () => finish();
|
|
327
|
+
const handleError = (error) => finish(error);
|
|
328
|
+
const handleAborted = () => finish(admissionAbortError(signal));
|
|
329
|
+
const handleSignalAbort = () => stopReading(admissionAbortError(signal));
|
|
330
|
+
|
|
331
|
+
req.once("end", handleEnd);
|
|
332
|
+
req.once("error", handleError);
|
|
333
|
+
req.once("aborted", handleAborted);
|
|
334
|
+
signal?.addEventListener("abort", handleSignalAbort, { once: true });
|
|
335
|
+
if (signal?.aborted) handleSignalAbort();
|
|
336
|
+
else {
|
|
337
|
+
req.on("data", handleData);
|
|
338
|
+
req.resume?.();
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function readRequestBuffer(req, maxBytes, signal) {
|
|
181
344
|
const contentLength = requestContentLength(req);
|
|
182
345
|
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
183
346
|
throw payloadTooLarge("Raw", maxBytes);
|
|
@@ -185,16 +348,18 @@ async function readRequestBuffer(req, maxBytes) {
|
|
|
185
348
|
|
|
186
349
|
const chunks = [];
|
|
187
350
|
let total = 0;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
351
|
+
await consumeRequestChunks(req, {
|
|
352
|
+
signal,
|
|
353
|
+
onChunk(buffer) {
|
|
354
|
+
total += buffer.length;
|
|
355
|
+
if (total > maxBytes) throw payloadTooLarge("Raw", maxBytes);
|
|
356
|
+
chunks.push(buffer);
|
|
357
|
+
},
|
|
358
|
+
});
|
|
194
359
|
return Buffer.concat(chunks);
|
|
195
360
|
}
|
|
196
361
|
|
|
197
|
-
async function readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes) {
|
|
362
|
+
async function readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes, signal) {
|
|
198
363
|
const contentLength = requestContentLength(req);
|
|
199
364
|
if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
|
|
200
365
|
throw payloadTooLarge("Raw", maxBodyBytes);
|
|
@@ -206,13 +371,15 @@ async function readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes) {
|
|
|
206
371
|
const decoder = new TextDecoder();
|
|
207
372
|
let text = "";
|
|
208
373
|
let total = 0;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
374
|
+
await consumeRequestChunks(req, {
|
|
375
|
+
signal,
|
|
376
|
+
onChunk(buffer) {
|
|
377
|
+
total += buffer.length;
|
|
378
|
+
if (total > maxBodyBytes) throw payloadTooLarge("Raw", maxBodyBytes);
|
|
379
|
+
if (total > maxDecodedBodyBytes) throw payloadTooLarge("Decoded", maxDecodedBodyBytes);
|
|
380
|
+
text += decoder.decode(buffer, { stream: true });
|
|
381
|
+
},
|
|
382
|
+
});
|
|
216
383
|
return text + decoder.decode();
|
|
217
384
|
}
|
|
218
385
|
|
|
@@ -267,22 +434,34 @@ function parseRequestJson(text) {
|
|
|
267
434
|
}
|
|
268
435
|
|
|
269
436
|
export async function readJsonBody(req, {
|
|
437
|
+
admission,
|
|
270
438
|
maxBodyBytes = MAX_BODY_BYTES,
|
|
271
439
|
maxDecodedBodyBytes = MAX_DECODED_BODY_BYTES,
|
|
440
|
+
signal,
|
|
272
441
|
} = {}) {
|
|
442
|
+
if (signal?.aborted) throw admissionAbortError(signal);
|
|
273
443
|
const encodings = contentEncodings(req.headers?.["content-encoding"]);
|
|
274
444
|
if (encodings.every((encoding) => encoding === "identity")) {
|
|
275
|
-
return parseRequestJson(await readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes));
|
|
445
|
+
return parseRequestJson(await readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes, signal));
|
|
276
446
|
}
|
|
277
|
-
const buffer = await readRequestBuffer(req, maxBodyBytes);
|
|
278
|
-
|
|
447
|
+
const buffer = await readRequestBuffer(req, maxBodyBytes, signal);
|
|
448
|
+
const releaseDecompression = await admission?.acquireDecompression?.({ signal }) || (() => {});
|
|
279
449
|
try {
|
|
280
|
-
decoded
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
450
|
+
let decoded;
|
|
451
|
+
try {
|
|
452
|
+
decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"], maxDecodedBodyBytes);
|
|
453
|
+
} catch (e) {
|
|
454
|
+
if (e?.statusCode) throw e;
|
|
455
|
+
throw httpError(`Invalid compressed request body: ${e.message}`, 400);
|
|
456
|
+
}
|
|
457
|
+
if (signal?.aborted) throw admissionAbortError(signal);
|
|
458
|
+
if (decoded.length > 0) {
|
|
459
|
+
await admission?.reserveDecodedBody?.(decoded.length, { signal });
|
|
460
|
+
}
|
|
461
|
+
return parseRequestJson(decoded.toString("utf8"));
|
|
462
|
+
} finally {
|
|
463
|
+
releaseDecompression();
|
|
284
464
|
}
|
|
285
|
-
return parseRequestJson(decoded.toString("utf8"));
|
|
286
465
|
}
|
|
287
466
|
|
|
288
467
|
export function sendJsonError(res, err, fallbackStatus = 400) {
|
|
@@ -291,7 +470,10 @@ export function sendJsonError(res, err, fallbackStatus = 400) {
|
|
|
291
470
|
res.end();
|
|
292
471
|
return;
|
|
293
472
|
}
|
|
294
|
-
|
|
473
|
+
const statusCode = err?.statusCode || fallbackStatus;
|
|
474
|
+
const headers = { "Content-Type": "application/json" };
|
|
475
|
+
if (statusCode === 408 || statusCode === 413) headers.Connection = "close";
|
|
476
|
+
res.writeHead(statusCode, headers);
|
|
295
477
|
res.end(JSON.stringify(err?.jsonBody || { error: err?.message || "Request failed" }));
|
|
296
478
|
}
|
|
297
479
|
|
|
@@ -327,6 +509,7 @@ export function isAbortLikeError(err) {
|
|
|
327
509
|
}
|
|
328
510
|
|
|
329
511
|
export function abortErrorStatusCode(reason) {
|
|
512
|
+
if (reason === "request_body_timeout") return 408;
|
|
330
513
|
if (["responses_prepare_timeout", "upstream_timeout", "stream_handshake_timeout", "stream_idle_timeout"].includes(reason)) return 504;
|
|
331
514
|
if (reason === "client_aborted" || reason === "client_closed") return 499;
|
|
332
515
|
return 502;
|
|
@@ -352,6 +535,7 @@ export function createRequestAbort(req, res) {
|
|
|
352
535
|
return {
|
|
353
536
|
signal: controller.signal,
|
|
354
537
|
get reason() { return reason; },
|
|
538
|
+
abort,
|
|
355
539
|
setTimeout(ms, nextReason = "upstream_timeout") {
|
|
356
540
|
if (timer) clearTimeout(timer);
|
|
357
541
|
timer = null;
|
package/src/launcher.mjs
CHANGED
|
@@ -3,9 +3,8 @@ import { status } from "./status.mjs";
|
|
|
3
3
|
|
|
4
4
|
export { ensureAuth } from "./auth.mjs";
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// Codex.app and the new ChatGPT.app regardless of where they are installed.
|
|
6
|
+
// Prefer the Codex bundle identifier, then fall back to the known app paths.
|
|
7
|
+
// This supports both Codex.app and ChatGPT.app regardless of installation path.
|
|
9
8
|
const CODEX_BUNDLE_ID = "com.openai.codex";
|
|
10
9
|
const APP_PATH_CANDIDATES = [
|
|
11
10
|
"/Applications/Codex.app",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { atomicWriteFileSync } from "./atomic-file.mjs";
|
|
5
|
+
|
|
6
|
+
export const LEGACY_COMMAND_WARNING_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
7
|
+
export const LEGACY_COMMAND_WARNING = "codex-copilot-dx is deprecated; use ccdx instead. The compatibility alias will be removed in a future breaking release.";
|
|
8
|
+
|
|
9
|
+
export function legacyCommandWarningPath({ env = process.env, home = os.homedir() } = {}) {
|
|
10
|
+
const xdgCacheHome = String(env.XDG_CACHE_HOME || "").trim();
|
|
11
|
+
const cacheRoot = xdgCacheHome || path.join(home, ".cache");
|
|
12
|
+
return path.join(cacheRoot, "codex-copilot-dx", "legacy-command-warning");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function shouldShowLegacyCommandWarning({
|
|
16
|
+
env = process.env,
|
|
17
|
+
home = os.homedir(),
|
|
18
|
+
interactive = process.stderr.isTTY === true,
|
|
19
|
+
now = Date.now(),
|
|
20
|
+
} = {}) {
|
|
21
|
+
if (!interactive) return false;
|
|
22
|
+
const filePath = legacyCommandWarningPath({ env, home });
|
|
23
|
+
try {
|
|
24
|
+
const previous = Number.parseInt(fs.readFileSync(filePath, "utf8"), 10);
|
|
25
|
+
if (Number.isFinite(previous) && Math.abs(now - previous) < LEGACY_COMMAND_WARNING_INTERVAL_MS) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// A missing or unreadable cache must not prevent the compatibility command.
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
atomicWriteFileSync(filePath, `${now}\n`, { mode: 0o600 });
|
|
33
|
+
} catch {
|
|
34
|
+
// The warning remains best effort when the cache directory is read-only.
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
package/src/pm-studio-relay.mjs
CHANGED
|
@@ -252,6 +252,9 @@ async function relayUpstreamResponse(response, reqBody, res, abort, streamIdleTi
|
|
|
252
252
|
function safeFailure(error, abort) {
|
|
253
253
|
if (error?.statusCode && !isAbortLikeError(error)) return error;
|
|
254
254
|
if (abort?.signal.aborted || isAbortLikeError(error)) {
|
|
255
|
+
if (abort?.reason === "request_body_timeout") {
|
|
256
|
+
return localError(408, "request_timeout", "PM Studio relay request body timed out");
|
|
257
|
+
}
|
|
255
258
|
return localError(
|
|
256
259
|
abortErrorStatusCode(abort?.reason),
|
|
257
260
|
"upstream_timeout",
|
|
@@ -277,6 +280,7 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
277
280
|
const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, runtime.upstreamTimeoutMs);
|
|
278
281
|
const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, runtime.streamHandshakeTimeoutMs);
|
|
279
282
|
const streamIdleTimeoutMs = parsePositiveInteger(options.streamIdleTimeoutMs, runtime.streamIdleTimeoutMs);
|
|
283
|
+
const requestBodyTimeoutMs = parsePositiveInteger(options.requestBodyTimeoutMs, runtime.requestBodyTimeoutMs);
|
|
280
284
|
const validationTtlMs = parsePositiveInteger(options.validationTtlMs, DEFAULT_VALIDATION_TTL_MS);
|
|
281
285
|
const validationCacheSize = parsePositiveInteger(options.validationCacheSize, DEFAULT_VALIDATION_CACHE_SIZE, 4096);
|
|
282
286
|
const validationConcurrency = parsePositiveInteger(
|
|
@@ -430,8 +434,10 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
430
434
|
async function handlePost(req, res, pathname, token, abort) {
|
|
431
435
|
let releaseRequest = releaseOnce();
|
|
432
436
|
try {
|
|
433
|
-
|
|
434
|
-
|
|
437
|
+
const admission = await acquireRequest(req, { signal: abort.signal });
|
|
438
|
+
releaseRequest = releaseOnce(admission);
|
|
439
|
+
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
440
|
+
const body = await readJsonBody(req, { admission, signal: abort.signal });
|
|
435
441
|
const supportsStream = pathname === "/chat/completions" || pathname === "/responses";
|
|
436
442
|
const streaming = supportsStream && body?.stream === true;
|
|
437
443
|
const requestTimeoutMs = streaming ? streamHandshakeTimeoutMs : upstreamTimeoutMs;
|
package/src/response-history.mjs
CHANGED
|
@@ -178,7 +178,7 @@ export function responseHistoryRootId(responseId) {
|
|
|
178
178
|
return histories.get(responseId)?.rootId || null;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
function responseHistoryChain(responseId) {
|
|
182
182
|
const chain = [];
|
|
183
183
|
const seen = new Set();
|
|
184
184
|
let currentId = responseId;
|
|
@@ -193,7 +193,16 @@ export function materializeResponseHistory(responseId) {
|
|
|
193
193
|
chain.push(entry);
|
|
194
194
|
currentId = entry.parentId;
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
return { chain, rootId: chain[0]?.rootId };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function responseHistoryMaterializedBytes(responseId) {
|
|
200
|
+
const { chain } = responseHistoryChain(responseId);
|
|
201
|
+
return chain.reduce((bytes, entry) => bytes + entry.bytes, 0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function materializeResponseHistory(responseId) {
|
|
205
|
+
const { chain, rootId } = responseHistoryChain(responseId);
|
|
197
206
|
const items = [];
|
|
198
207
|
for (const entry of chain.reverse()) items.push(...entry.inputItems, ...entry.outputItems);
|
|
199
208
|
const materialized = cloneJson(items);
|
package/src/responses-bridge.mjs
CHANGED
|
@@ -143,23 +143,38 @@ export function chatToResponses(chatResp, model) {
|
|
|
143
143
|
|
|
144
144
|
export async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {}) {
|
|
145
145
|
delete chatReq.max_tokens;
|
|
146
|
-
const
|
|
147
|
-
|
|
146
|
+
const requestModel = chatReq.model || "unknown";
|
|
147
|
+
let upstreamReq = withChatStreamUsage(chatReq);
|
|
148
|
+
let {
|
|
149
|
+
abort,
|
|
150
|
+
bodyText,
|
|
151
|
+
chatCompletionsFn: configuredChatCompletionsFn,
|
|
152
|
+
onUpstreamResponse,
|
|
153
|
+
onUpstreamStart,
|
|
154
|
+
releaseRequest,
|
|
155
|
+
signal,
|
|
156
|
+
streamIdleTimeoutMs,
|
|
157
|
+
} = options;
|
|
158
|
+
options = undefined;
|
|
159
|
+
if (upstreamReq !== chatReq) bodyText = undefined;
|
|
148
160
|
let resp;
|
|
149
161
|
try {
|
|
150
|
-
const chatCompletionsFn =
|
|
162
|
+
const chatCompletionsFn = configuredChatCompletionsFn || chatCompletions;
|
|
151
163
|
try {
|
|
152
|
-
|
|
164
|
+
onUpstreamStart?.();
|
|
153
165
|
resp = await chatCompletionsFn(upstreamReq, {
|
|
154
|
-
signal
|
|
166
|
+
signal,
|
|
155
167
|
bodyText,
|
|
156
|
-
onUpstreamStart
|
|
168
|
+
onUpstreamStart,
|
|
157
169
|
});
|
|
158
170
|
} finally {
|
|
159
|
-
|
|
171
|
+
bodyText = undefined;
|
|
172
|
+
chatReq = undefined;
|
|
173
|
+
upstreamReq = undefined;
|
|
174
|
+
releaseRequest?.();
|
|
160
175
|
}
|
|
161
176
|
} catch (e) {
|
|
162
|
-
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(
|
|
177
|
+
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(abort?.reason) : 502;
|
|
163
178
|
await onError(statusCode, e.message);
|
|
164
179
|
return false;
|
|
165
180
|
}
|
|
@@ -173,10 +188,10 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
173
188
|
await onError(error.statusCode, error.message, error);
|
|
174
189
|
return false;
|
|
175
190
|
}
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
onUpstreamResponse?.(resp);
|
|
192
|
+
abort?.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout");
|
|
178
193
|
const respId = `resp_${uid()}`;
|
|
179
|
-
let actualModel =
|
|
194
|
+
let actualModel = requestModel;
|
|
180
195
|
let fullText = "";
|
|
181
196
|
let messageItem = null;
|
|
182
197
|
let nextOutputIndex = 0;
|
|
@@ -280,7 +295,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
280
295
|
|
|
281
296
|
try {
|
|
282
297
|
for await (const line of webStreamLines(resp, {
|
|
283
|
-
onChunk: () =>
|
|
298
|
+
onChunk: () => abort?.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout"),
|
|
284
299
|
})) {
|
|
285
300
|
if (!line.startsWith("data: ")) continue;
|
|
286
301
|
const data = line.slice(6).trim();
|
|
@@ -334,7 +349,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
334
349
|
}
|
|
335
350
|
}
|
|
336
351
|
} catch (e) {
|
|
337
|
-
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(
|
|
352
|
+
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(abort?.reason) : (e?.statusCode || 502);
|
|
338
353
|
await onError(statusCode, e?.message || "upstream stream error", e);
|
|
339
354
|
return false;
|
|
340
355
|
}
|
|
@@ -17,7 +17,12 @@ import { chatToResponses, forwardToChat, responsesBodyUsesCustomTools } from "./
|
|
|
17
17
|
import { prepareResponsesChatPayload } from "./responses-chat-payload.mjs";
|
|
18
18
|
import { prepareResponsesCompactionRequest } from "./responses-compaction.mjs";
|
|
19
19
|
import { proxyCopilotResponses } from "./responses-proxy.mjs";
|
|
20
|
-
import {
|
|
20
|
+
import { responseHistoryMaterializedBytes } from "./response-history.mjs";
|
|
21
|
+
import {
|
|
22
|
+
dropMaterializedResponseHistory,
|
|
23
|
+
prepareResponsesRequest,
|
|
24
|
+
rememberResponseHistory,
|
|
25
|
+
} from "./responses-request.mjs";
|
|
21
26
|
import { status } from "./status.mjs";
|
|
22
27
|
import { endStreamWithError } from "./stream-errors.mjs";
|
|
23
28
|
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
@@ -81,12 +86,14 @@ export function createResponsesHandler(options) {
|
|
|
81
86
|
getCachedModelEndpointsFn = getCachedModelEndpoints,
|
|
82
87
|
imagePressure,
|
|
83
88
|
openAIModelEnv,
|
|
89
|
+
requestBodyTimeoutMs,
|
|
84
90
|
responsesPayloadOptions,
|
|
85
91
|
responsesFn,
|
|
86
92
|
streamHandshakeTimeoutMs,
|
|
87
93
|
streamIdleTimeoutMs,
|
|
88
94
|
upstreamTimeoutMs,
|
|
89
95
|
} = options;
|
|
96
|
+
const now = options.now || Date.now;
|
|
90
97
|
|
|
91
98
|
return async function handleResponses(req, res) {
|
|
92
99
|
const abort = createRequestAbort(req, res);
|
|
@@ -94,10 +101,26 @@ export function createResponsesHandler(options) {
|
|
|
94
101
|
let prepared = null;
|
|
95
102
|
let imagePressureResult = null;
|
|
96
103
|
let parsed = null;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
104
|
+
let upstreamPayloadReleased = false;
|
|
105
|
+
let prepareDeadline = Number.POSITIVE_INFINITY;
|
|
106
|
+
let upstreamStarted = false;
|
|
107
|
+
const assertPrepareActive = () => {
|
|
108
|
+
if (abort.signal.aborted) throw abort.signal.reason;
|
|
109
|
+
if (now() < prepareDeadline) return;
|
|
110
|
+
abort.abort("responses_prepare_timeout");
|
|
111
|
+
throw abort.signal.reason;
|
|
112
|
+
};
|
|
113
|
+
const startUpstreamTimeout = () => {
|
|
114
|
+
if (!upstreamStarted) {
|
|
115
|
+
assertPrepareActive();
|
|
116
|
+
upstreamStarted = true;
|
|
117
|
+
}
|
|
118
|
+
const streaming = prepared?.body?.stream === true;
|
|
119
|
+
abort.setTimeout(
|
|
120
|
+
streaming ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
121
|
+
streaming ? "stream_handshake_timeout" : "upstream_timeout",
|
|
122
|
+
);
|
|
123
|
+
};
|
|
101
124
|
const applyAdaptiveTimeout = (error) => {
|
|
102
125
|
if (!["responses_prepare_timeout", "stream_handshake_timeout", "upstream_timeout"].includes(abort.reason)) {
|
|
103
126
|
return error;
|
|
@@ -118,21 +141,47 @@ export function createResponsesHandler(options) {
|
|
|
118
141
|
};
|
|
119
142
|
return error;
|
|
120
143
|
};
|
|
144
|
+
const markAdaptiveHttpTimeout = (statusCode) => {
|
|
145
|
+
if (statusCode !== 408) return false;
|
|
146
|
+
return imagePressure?.markTimeout?.(prepared?.historyRootId, {
|
|
147
|
+
eligible: imagePressureResult?.pressureEligible === true,
|
|
148
|
+
}) || false;
|
|
149
|
+
};
|
|
150
|
+
const releaseUpstreamPayload = (finalContext) => {
|
|
151
|
+
if (upstreamPayloadReleased) return;
|
|
152
|
+
upstreamPayloadReleased = true;
|
|
153
|
+
if (finalContext && finalContext !== prepared) dropMaterializedResponseHistory(finalContext);
|
|
154
|
+
dropMaterializedResponseHistory(prepared);
|
|
155
|
+
releaseRequest();
|
|
156
|
+
};
|
|
121
157
|
try {
|
|
122
158
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
123
|
-
|
|
159
|
+
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
160
|
+
parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
161
|
+
prepareDeadline = now() + upstreamTimeoutMs;
|
|
124
162
|
abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
|
|
163
|
+
if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
|
|
164
|
+
const historyBytes = responseHistoryMaterializedBytes(parsed.previous_response_id);
|
|
165
|
+
if (historyBytes > 0) {
|
|
166
|
+
await releaseRequest.reserveResponseHistory?.(historyBytes, { signal: abort.signal });
|
|
167
|
+
}
|
|
168
|
+
assertPrepareActive();
|
|
169
|
+
}
|
|
125
170
|
prepared = prepareResponsesRequest(parsed, { mutate: true });
|
|
171
|
+
parsed = null;
|
|
172
|
+
assertPrepareActive();
|
|
126
173
|
imagePressureResult = imagePressure?.apply?.(prepared) || null;
|
|
174
|
+
assertPrepareActive();
|
|
127
175
|
if (imagePressureResult?.adapted) {
|
|
128
176
|
console.warn(status("warn", `responses visual history mode=${imagePressureResult.mode} historical_images=${imagePressureResult.initialHistoricalImages}->${imagePressureResult.historicalImages} omitted=${imagePressureResult.imagesOmitted} bytes=${imagePressureResult.initialBodyBytes}->${imagePressureResult.bodyBytes}`));
|
|
129
177
|
}
|
|
130
178
|
prepared.surface = "responses";
|
|
131
|
-
const model =
|
|
179
|
+
const model = prepared.body.model || "unknown";
|
|
180
|
+
const streaming = prepared.body.stream === true;
|
|
132
181
|
const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
|
|
133
182
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
134
183
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
135
|
-
console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${
|
|
184
|
+
console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${streaming}`));
|
|
136
185
|
const usesCustomTools = responsesBodyUsesCustomTools(prepared.body);
|
|
137
186
|
const useNativeResponses = requestedModel === CODEX_AUTO_REVIEW_MODEL
|
|
138
187
|
|| isResponsesOnlyModel(upstreamModel, getCachedModelEndpointsFn)
|
|
@@ -143,18 +192,23 @@ export function createResponsesHandler(options) {
|
|
|
143
192
|
signal: abort.signal,
|
|
144
193
|
abort,
|
|
145
194
|
onUpstreamStart: startUpstreamTimeout,
|
|
146
|
-
releaseRequest,
|
|
195
|
+
releaseRequest: releaseUpstreamPayload,
|
|
147
196
|
streamIdleTimeoutMs,
|
|
148
197
|
});
|
|
149
198
|
if (result?.successful) imagePressure?.markSuccess?.(prepared.historyRootId);
|
|
199
|
+
else markAdaptiveHttpTimeout(result?.upstreamStatus);
|
|
150
200
|
} else {
|
|
151
|
-
|
|
201
|
+
let { chatReq, bodyText } = await prepareResponsesChatPayload(prepared, {
|
|
152
202
|
payloadOptions: responsesPayloadOptions,
|
|
153
203
|
signal: abort.signal,
|
|
154
|
-
stream:
|
|
204
|
+
stream: streaming,
|
|
155
205
|
});
|
|
156
|
-
const
|
|
157
|
-
|
|
206
|
+
const releaseChatPayload = () => {
|
|
207
|
+
chatReq = undefined;
|
|
208
|
+
bodyText = undefined;
|
|
209
|
+
releaseUpstreamPayload();
|
|
210
|
+
};
|
|
211
|
+
if (streaming) {
|
|
158
212
|
let streamResponseHeaders = null;
|
|
159
213
|
const successful = await forwardToChat(chatReq, async (event, data) => {
|
|
160
214
|
if (!res.headersSent) res.writeHead(200, {
|
|
@@ -170,6 +224,7 @@ export function createResponsesHandler(options) {
|
|
|
170
224
|
}, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg, error, upstreamResponse) => {
|
|
171
225
|
if (!res.headersSent) {
|
|
172
226
|
if (upstreamResponse) {
|
|
227
|
+
markAdaptiveHttpTimeout(upstreamResponse.status);
|
|
173
228
|
sendUpstreamError(res, upstreamResponse, errMsg);
|
|
174
229
|
return;
|
|
175
230
|
}
|
|
@@ -184,7 +239,7 @@ export function createResponsesHandler(options) {
|
|
|
184
239
|
onUpstreamStart: startUpstreamTimeout,
|
|
185
240
|
streamIdleTimeoutMs,
|
|
186
241
|
chatCompletionsFn,
|
|
187
|
-
releaseRequest,
|
|
242
|
+
releaseRequest: releaseChatPayload,
|
|
188
243
|
bodyText,
|
|
189
244
|
onUpstreamResponse: (upstreamResponse) => {
|
|
190
245
|
streamResponseHeaders = safeUpstreamResponseHeaders(upstreamResponse.headers, {
|
|
@@ -201,9 +256,10 @@ export function createResponsesHandler(options) {
|
|
|
201
256
|
bodyText,
|
|
202
257
|
onUpstreamStart: startUpstreamTimeout,
|
|
203
258
|
});
|
|
204
|
-
|
|
259
|
+
releaseChatPayload();
|
|
205
260
|
const data = await upstream.text();
|
|
206
261
|
if (!upstream.ok) {
|
|
262
|
+
markAdaptiveHttpTimeout(upstream.status);
|
|
207
263
|
sendUpstreamError(res, upstream, data);
|
|
208
264
|
return;
|
|
209
265
|
}
|
|
@@ -227,7 +283,7 @@ export function createResponsesHandler(options) {
|
|
|
227
283
|
logRequestFailure("Responses", responseError, abort);
|
|
228
284
|
sendJsonError(res, responseError, 502);
|
|
229
285
|
} finally {
|
|
230
|
-
|
|
286
|
+
releaseUpstreamPayload();
|
|
231
287
|
abort.cleanup();
|
|
232
288
|
}
|
|
233
289
|
};
|
|
@@ -239,20 +295,46 @@ export function createResponsesCompactHandler(options) {
|
|
|
239
295
|
autoReviewModelResolver,
|
|
240
296
|
imagePressure,
|
|
241
297
|
openAIModelEnv,
|
|
298
|
+
requestBodyTimeoutMs,
|
|
242
299
|
responsesCompactFn,
|
|
243
300
|
upstreamTimeoutMs,
|
|
244
301
|
} = options;
|
|
302
|
+
const now = options.now || Date.now;
|
|
245
303
|
|
|
246
304
|
return async function handleResponsesCompact(req, res) {
|
|
247
305
|
const abort = createRequestAbort(req, res);
|
|
248
306
|
let releaseRequest = () => {};
|
|
249
307
|
try {
|
|
250
308
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
251
|
-
|
|
309
|
+
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
310
|
+
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
311
|
+
const prepareDeadline = now() + upstreamTimeoutMs;
|
|
312
|
+
const assertPrepareActive = () => {
|
|
313
|
+
if (abort.signal.aborted) throw abort.signal.reason;
|
|
314
|
+
if (now() < prepareDeadline) return;
|
|
315
|
+
abort.abort("responses_prepare_timeout");
|
|
316
|
+
throw abort.signal.reason;
|
|
317
|
+
};
|
|
318
|
+
let upstreamStarted = false;
|
|
319
|
+
const startUpstreamTimeout = () => {
|
|
320
|
+
if (!upstreamStarted) {
|
|
321
|
+
assertPrepareActive();
|
|
322
|
+
upstreamStarted = true;
|
|
323
|
+
}
|
|
324
|
+
abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
|
|
325
|
+
};
|
|
252
326
|
abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
|
|
327
|
+
if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
|
|
328
|
+
const historyBytes = responseHistoryMaterializedBytes(parsed.previous_response_id);
|
|
329
|
+
if (historyBytes > 0) {
|
|
330
|
+
await releaseRequest.reserveResponseHistory?.(historyBytes, { signal: abort.signal });
|
|
331
|
+
}
|
|
332
|
+
assertPrepareActive();
|
|
333
|
+
}
|
|
253
334
|
const prepared = prepareResponsesCompactionRequest(
|
|
254
335
|
prepareResponsesRequest(parsed, { mutate: true }),
|
|
255
336
|
);
|
|
337
|
+
assertPrepareActive();
|
|
256
338
|
prepared.surface = "responses_compact";
|
|
257
339
|
const model = parsed.model || "unknown";
|
|
258
340
|
const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
|
|
@@ -262,7 +344,7 @@ export function createResponsesCompactHandler(options) {
|
|
|
262
344
|
const result = await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
|
|
263
345
|
signal: abort.signal,
|
|
264
346
|
abort,
|
|
265
|
-
onUpstreamStart:
|
|
347
|
+
onUpstreamStart: startUpstreamTimeout,
|
|
266
348
|
releaseRequest,
|
|
267
349
|
});
|
|
268
350
|
if (result?.compacted) imagePressure?.clear?.(prepared.historyRootId);
|
|
@@ -32,8 +32,24 @@ export function applyResponsesImagePressure(reqContext, {
|
|
|
32
32
|
const currentInputStart = Number.isFinite(reqContext?.currentInputStart)
|
|
33
33
|
? reqContext.currentInputStart
|
|
34
34
|
: 0;
|
|
35
|
-
const initial = bodyPayload(body);
|
|
36
35
|
const initialImages = responsesHistoricalImageStats(body?.input, currentInputStart);
|
|
36
|
+
if (initialImages.historicalImages === 0) {
|
|
37
|
+
return {
|
|
38
|
+
mode: "normal",
|
|
39
|
+
pressureEligible: false,
|
|
40
|
+
adapted: false,
|
|
41
|
+
imagesOmitted: 0,
|
|
42
|
+
initialBodyBytes: null,
|
|
43
|
+
bodyBytes: null,
|
|
44
|
+
initialHistoricalImages: 0,
|
|
45
|
+
historicalImages: 0,
|
|
46
|
+
currentImages: initialImages.currentImages,
|
|
47
|
+
overBudget: false,
|
|
48
|
+
overImageBudget: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const initial = bodyPayload(body);
|
|
37
53
|
const pressureEligible = initialImages.historicalImages > 0 && (
|
|
38
54
|
initialImages.historicalImages > policy.triggerHistoricalImages
|
|
39
55
|
|| initial.bodyBytes > policy.triggerBodyBytes
|
|
@@ -70,12 +86,13 @@ export function applyResponsesImagePressure(reqContext, {
|
|
|
70
86
|
initialBodyText: initial.bodyText,
|
|
71
87
|
initialBodyBytes: initial.bodyBytes,
|
|
72
88
|
});
|
|
89
|
+
const { bodyText: _bodyText, ...trimmedResult } = trimmed;
|
|
73
90
|
return {
|
|
74
91
|
mode: recovery ? "recovery" : "pressure",
|
|
75
92
|
pressureEligible: pressureEligible || recovery,
|
|
76
93
|
initialBodyBytes: initial.bodyBytes,
|
|
77
94
|
initialHistoricalImages: initialImages.historicalImages,
|
|
78
|
-
...
|
|
95
|
+
...trimmedResult,
|
|
79
96
|
};
|
|
80
97
|
}
|
|
81
98
|
|
package/src/responses-proxy.mjs
CHANGED
|
@@ -120,13 +120,13 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
|
|
|
120
120
|
try {
|
|
121
121
|
opened = await openCopilotResponse(reqContext, upstream, options);
|
|
122
122
|
} finally {
|
|
123
|
-
options.releaseRequest?.();
|
|
123
|
+
options.releaseRequest?.(opened?.reqContext);
|
|
124
124
|
}
|
|
125
125
|
const { resp, errorText } = opened;
|
|
126
126
|
reqContext = opened.reqContext;
|
|
127
127
|
if (errorText !== undefined) {
|
|
128
128
|
sendUpstreamError(res, resp, errorText);
|
|
129
|
-
return { successful: false, compacted: false };
|
|
129
|
+
return { successful: false, compacted: false, upstreamStatus: resp.status };
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
if (reqContext.body.stream) {
|
|
@@ -245,6 +245,14 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
|
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
export function dropMaterializedResponseHistory(reqContext) {
|
|
249
|
+
if (!reqContext?.historyParentId || !Array.isArray(reqContext.historyInputItems)) return false;
|
|
250
|
+
reqContext.body = { ...reqContext.body, input: reqContext.historyInputItems };
|
|
251
|
+
reqContext.inputItems = reqContext.historyInputItems;
|
|
252
|
+
reqContext.currentInputStart = 0;
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
248
256
|
export function rememberResponseHistory(reqContext, responseJson) {
|
|
249
257
|
if (!responseJson?.id || !Array.isArray(reqContext?.historyInputItems || reqContext?.inputItems)) return;
|
|
250
258
|
const compacted = isSuccessfulCompactionResponse(responseJson);
|
package/src/runtime-config.mjs
CHANGED
|
@@ -2,6 +2,7 @@ export const RUNTIME_DEFAULTS = Object.freeze({
|
|
|
2
2
|
upstreamTimeoutMs: 120 * 1000,
|
|
3
3
|
streamHandshakeTimeoutMs: 120 * 1000,
|
|
4
4
|
streamIdleTimeoutMs: 120 * 1000,
|
|
5
|
+
requestBodyTimeoutMs: 120 * 1000,
|
|
5
6
|
maxSseBufferBytes: 8 * 1024 * 1024,
|
|
6
7
|
maxBodyBytes: 64 * 1024 * 1024,
|
|
7
8
|
maxDecodedBodyBytes: 128 * 1024 * 1024,
|
|
@@ -24,6 +25,7 @@ export function loadRuntimeConfig(env = process.env) {
|
|
|
24
25
|
upstreamTimeoutMs: parsePositiveInteger(env.CCDX_UPSTREAM_TIMEOUT_MS, RUNTIME_DEFAULTS.upstreamTimeoutMs),
|
|
25
26
|
streamHandshakeTimeoutMs: parsePositiveInteger(env.CCDX_STREAM_HANDSHAKE_TIMEOUT_MS, RUNTIME_DEFAULTS.streamHandshakeTimeoutMs),
|
|
26
27
|
streamIdleTimeoutMs: parsePositiveInteger(env.CCDX_STREAM_IDLE_TIMEOUT_MS, RUNTIME_DEFAULTS.streamIdleTimeoutMs),
|
|
28
|
+
requestBodyTimeoutMs: parsePositiveInteger(env.CCDX_REQUEST_BODY_TIMEOUT_MS, RUNTIME_DEFAULTS.requestBodyTimeoutMs),
|
|
27
29
|
maxSseBufferBytes: parsePositiveInteger(env.CCDX_MAX_SSE_BUFFER_BYTES, RUNTIME_DEFAULTS.maxSseBufferBytes),
|
|
28
30
|
maxBodyBytes: parsePositiveInteger(env.CCDX_MAX_BODY_BYTES, RUNTIME_DEFAULTS.maxBodyBytes),
|
|
29
31
|
maxDecodedBodyBytes: parsePositiveInteger(env.CCDX_MAX_DECODED_BODY_BYTES, RUNTIME_DEFAULTS.maxDecodedBodyBytes),
|