Skip to content

Commit 6a6ba06

Browse files
committed
release: v2.7.6
1 parent 853e7fe commit 6a6ba06

2 files changed

Lines changed: 22 additions & 13 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bitkyc08/opencodex",
3-
"version": "2.7.4",
3+
"version": "2.7.6",
44
"description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
55
"type": "module",
66
"main": "./bin/package-main.mjs",

src/server/images.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { formatCodexProviderForLog } from "../codex/routing";
2424
import { resolveEnvValue } from "../config";
2525
import { signalWithTimeout } from "../lib/abort";
2626
import { sidecarEnter } from "../lib/sidecar-tracker";
27-
import { fetchWithResetRetry } from "../lib/upstream-retry";
2827
import type { OcxConfig, OcxProviderConfig } from "../types";
2928
import { isProxyAdmissionSecret } from "./auth-cors";
3029
import { readJsonRequestBody } from "./request-decompress";
@@ -36,6 +35,13 @@ export type ImagesEndpoint = "generations" | "edits";
3635
/** Image generation is slow (tens of seconds); bound a hung upstream, not a working one. */
3736
const IMAGES_UPSTREAM_TIMEOUT_MS = 300_000;
3837

38+
/**
39+
* Cap for the buffered upstream response body (100 MiB). Images responses are JSON documents
40+
* containing base64-encoded images — typically a few MB. This prevents an oversized or malicious
41+
* response from exhausting process memory.
42+
*/
43+
const IMAGES_RESPONSE_MAX_BYTES = 100 * 1024 * 1024;
44+
3945
interface NamedProvider {
4046
name: string;
4147
provider: OcxProviderConfig;
@@ -168,18 +174,21 @@ export async function handleImages(
168174
const linkedSignal = signalWithTimeout(timeoutMs, req.signal);
169175
const sidecarExit = sidecarEnter("images");
170176
try {
171-
const upstreamResponse = await fetchWithResetRetry(
172-
() => fetch(url, {
173-
method: "POST",
174-
headers,
175-
body: JSON.stringify(body),
176-
signal: linkedSignal.signal,
177-
}),
178-
{ abortSignal: linkedSignal.signal, label: "images-relay" },
179-
);
180-
// Buffer rather than stream: the payload is one JSON document (base64 image, a few MB),
181-
// and buffering keeps the timeout window covering the whole exchange.
177+
// Images POSTs create paid, non-idempotent work. One fetch only: no reset retry without a
178+
// source-proven idempotency contract.
179+
const upstreamResponse = await fetch(url, {
180+
method: "POST",
181+
headers,
182+
body: JSON.stringify(body),
183+
signal: linkedSignal.signal,
184+
});
185+
// Buffer rather than stream: the payload is one JSON document (base64 image, typically a few
186+
// MB), and buffering keeps the timeout window covering the whole exchange. Cap the size to
187+
// prevent an oversized response from exhausting process memory.
182188
const payload = await upstreamResponse.arrayBuffer();
189+
if (payload.byteLength > IMAGES_RESPONSE_MAX_BYTES) {
190+
return formatErrorResponse(502, "upstream_error", `image ${endpoint} response too large (${payload.byteLength} bytes)`);
191+
}
183192
recordOutcome?.(upstreamResponse.status);
184193
const relayHeaders: Record<string, string> = {};
185194
const contentType = upstreamResponse.headers.get("content-type");

0 commit comments

Comments
 (0)