Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/reference/proxy-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ should select among several targets.
This is the native CodexCommander data-plane shape. The request body must be a JSON object with a
non-empty `model`. `input` may be a string or an array of Responses items.

Responses HTTP request bodies may be up to 256 MiB, both on the wire and after decoding
`Content-Encoding`. The HTTP listener and JSON parser use the same limit, so image-heavy histories
between 128 and 256 MiB can reach request processing. Larger bodies receive HTTP 413. Upstream
providers may impose additional request-size or context limits.

### Accepted request fields

| Area | Accepted shape |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codexcommander",
"version": "0.1.21",
"version": "0.1.22",
"private": true,
"description": "CodexCommander — universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
"type": "module",
Expand Down
2 changes: 2 additions & 0 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export {
} from "./auth-cors";
import { disableResponsesRequestTimeout, handleResponses, handleResponsesCompact } from "./responses";
export { disableResponsesRequestTimeout, linkAbortSignal } from "./responses";
import { MAX_DECOMPRESSED_BODY_BYTES } from "./request-decompress";
import { handleClaudeCountTokens, handleClaudeMessages } from "./claude-messages";
import { handleChatCompletions } from "./chat-completions";
import { anthropicErrorResponse } from "../claude/outbound";
Expand Down Expand Up @@ -618,6 +619,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
port: listenPort,
hostname: bindHost,
idleTimeout: 255,
maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES,
async fetch(req, requestServer): Promise<Response> {
const url = new URL(req.url);
markActivity(`${req.method} ${url.pathname}`);
Expand Down
101 changes: 101 additions & 0 deletions tests/server-body-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { createConnection } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { saveConfig } from "../src/config";
import { startServer } from "../src/server";
import { MAX_DECOMPRESSED_BODY_BYTES } from "../src/server/request-decompress";
import { installIsolatedCodexHome, type IsolatedCodexHome } from "./helpers/isolated-codex-home";
import { INTERNAL_DEADLINE_MS, SERVER_BUDGET_MS } from "./helpers/test-budget";

const previousHome = process.env.CODEXCOMMANDER_HOME;
const previousApiToken = process.env.CODEXCOMMANDER_API_AUTH_TOKEN;
let testHome = "";
let codexHome: IsolatedCodexHome;

beforeEach(() => {
testHome = mkdtempSync(join(tmpdir(), "ccx-server-body-limit-"));
process.env.CODEXCOMMANDER_HOME = testHome;
process.env.CODEXCOMMANDER_API_AUTH_TOKEN = "body-limit-test-token";
codexHome = installIsolatedCodexHome("ccx-body-limit-codex-");
saveConfig({
port: 0,
hostname: "0.0.0.0",
multiAgentGuidanceEnabled: true,
clientIntegrations: { codex: false },
defaultProvider: "test",
providers: {
test: { adapter: "openai-chat", baseUrl: "https://example.test/v1", disabled: true },
},
});
});

afterEach(() => {
if (previousHome === undefined) delete process.env.CODEXCOMMANDER_HOME;
else process.env.CODEXCOMMANDER_HOME = previousHome;
if (previousApiToken === undefined) delete process.env.CODEXCOMMANDER_API_AUTH_TOKEN;
else process.env.CODEXCOMMANDER_API_AUTH_TOKEN = previousApiToken;
codexHome.restore();
rmSync(testHome, { recursive: true, force: true });
});

function requestWithDeclaredBodySize(port: number, bytes: number): Promise<{ status: number; raw: string }> {
return new Promise((resolve, reject) => {
let raw = "";
let settled = false;
const finish = (error?: Error) => {
if (settled) return;
settled = true;
socket.destroy();
if (error) return reject(error);
const status = Number(raw.match(/^HTTP\/1\.[01] (\d{3})/)?.[1] ?? 0);
resolve({ status, raw });
};
const socket = createConnection({ host: "127.0.0.1", port }, () => {
// Header-only requests let Bun enforce its real ingress limit without allocating
// hundreds of MiB. Below that limit, the unchanged auth gate responds before reading.
socket.write([
"POST /v1/responses HTTP/1.1",
`Host: 127.0.0.1:${port}`,
"Content-Type: application/json",
`Content-Length: ${bytes}`,
"Connection: close",
"",
"",
].join("\r\n"));
});
socket.setEncoding("utf8");
socket.setTimeout(INTERNAL_DEADLINE_MS, () => finish(new Error("body-limit request timed out")));
socket.on("data", chunk => { raw += chunk; });
socket.on("end", () => finish());
socket.on("error", error => finish(error));
});
}

describe("real server HTTP ingress body limit", () => {
test.each([
["above Bun's 128 MiB default", 128 * 1024 * 1024 + 1],
["exactly at the shared cap", MAX_DECOMPRESSED_BODY_BYTES],
])("admits declared bodies %s to the application handler", async (_label, bytes) => {
const server = startServer(0);
try {
const response = await requestWithDeclaredBodySize(server.port!, bytes);
expect(response.status).toBe(401);
expect(response.raw).toContain('"type":"authentication_error"');
} finally {
await server.stop(true);
}
}, SERVER_BUDGET_MS);

test("rejects a declared body one byte above the shared cap at ingress", async () => {
const server = startServer(0);
try {
const response = await requestWithDeclaredBodySize(server.port!, MAX_DECOMPRESSED_BODY_BYTES + 1);
expect(response.status).toBe(413);
expect(response.raw).not.toContain('"type":"authentication_error"');
} finally {
await server.stop(true);
}
}, SERVER_BUDGET_MS);
});
Loading