[Bug] Explicit stallTimeoutSec is not enforced for native Responses SSE while translated Chat honors it
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- bun, typescript
- Domain
- backend-api-design
Research direction
Read src/server/responses/passthrough-execution.ts and src/lib/response-body-inactivity.ts, then trace native relay ownership in src/server/responses/passthrough-delivery.ts. Reproduce with the provided .tmp/stall-repro.ts fixture and compare native Responses with translated Chat. Done when the configured one-second timeout is enforced consistently, or the documented contract clearly explains the native route’s exclusion.
Written by the indexing model from the issue text.
Description
Client or integration
Direct HTTP/API client; the affected route is also used by Codex App.
Area
Streaming
Summary
An explicit stallTimeoutSec: 1 does not bound a native openai-responses SSE body in this local fixture. The otherwise comparable openai-chat translated route stops after about one second.
The mock upstream sends a protocol prelude immediately, then remains silent for 4.5 seconds before returning a completed marker. Both destinations are loopback and both receive the same explicit positive budget. This is not the intentional "unset local upstream budget is disabled" case.
Observed:
- native Responses: HTTP 200, completes with marker at 4.562 s;
- translated Chat: streaming HTTP 200 envelope, terminal
response.failedat 1.040 s, no marker.
The server configuration documentation describes the stall setting as covering Responses and native Chat, including an explicit positive value for local upstreams. Please confirm and align the native streaming contract, or document its exclusion clearly if intentional. This report does not claim the inconsistency caused a particular remote-provider EOF.
Reproduction
Use a source checkout with dependencies installed. Save the fixture below as .tmp/stall-repro.ts, then run:
bun .tmp/stall-repro.ts
It starts two ephemeral loopback servers, uses only synthetic input and a dummy key, isolates OpenCodex/Codex homes under the fixture directory, and stops its servers. It does not call an external model or use a user's real gateway.
Self-contained local fixture
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const root = import.meta.dir;
const fixtureHome = join(root, "stall-fixture-home");
mkdirSync(fixtureHome, { recursive: true, mode: 0o700 });
process.env.OPENCODEX_HOME = fixtureHome;
process.env.CODEX_HOME = join(fixtureHome, "codex");
process.env.CODEX_CI = "1";
const { saveConfig } = await import("../src/config");
const { startServer } = await import("../src/server");
const encoder = new TextEncoder();
const upstream = Bun.serve({
port: 0, hostname: "127.0.0.1", idleTimeout: 0,
fetch(req) {
const chat = new URL(req.url).pathname.includes("chat/completions");
let timer: ReturnType<typeof setTimeout> | undefined;
const stream = new ReadableStream<Uint8Array>({
start(controller) {
const first = chat
? { id: "fixture-chat", object: "chat.completion.chunk", choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }] }
: { type: "response.created", response: { id: "resp_fixture", object: "response", status: "in_progress", output: [] } };
controller.enqueue(encoder.encode(`data: ${JSON.stringify(first)}\n\n`));
timer = setTimeout(() => {
const last = chat
? { id: "fixture-chat", object: "chat.completion.chunk", choices: [{ index: 0, delta: { content: "FIXTURE_OK" }, finish_reason: "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }
: { type: "response.completed", response: { id: "resp_fixture", object: "response", status: "completed", model: "probe", output: [{ type: "message", id: "msg_fixture", role: "assistant", status: "completed", content: [{ type: "output_text", text: "FIXTURE_OK", annotations: [] }] }], usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 } } };
controller.enqueue(encoder.encode(`data: ${JSON.stringify(last)}\n\ndata: [DONE]\n\n`));
controller.close();
}, 4500);
},
cancel() { if (timer !== undefined) clearTimeout(timer); },
});
return new Response(stream, { headers: { "content-type": "text/event-stream" } });
},
});
const destination = { baseUrl: new URL("v1", upstream.url).href, apiKey: "fixture-key", allowPrivateNetwork: true, proxy: "direct" as const, models: ["probe"], defaultAliases: false };
saveConfig({
port: 0, hostname: "127.0.0.1", defaultProvider: "fixture-native", defaultModelAliases: false,
providers: { "fixture-native": { ...destination, adapter: "openai-responses" }, "fixture-chat": { ...destination, adapter: "openai-chat" } },
clientIntegrations: { codex: false, grok: false, "claude-desktop": false },
codexAutoStart: false, codexShimAutoRestore: false, syncResumeHistory: false,
emptyCompletionRetry: false, stallTimeoutSec: 1,
});
const gateway = startServer(0);
const results = [];
try {
for (const provider of ["fixture-native", "fixture-chat"]) {
const start = performance.now();
const response = await fetch(new URL("v1/responses", gateway.url), {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ model: provider + "/probe", input: "Synthetic test", stream: true, store: false }),
signal: AbortSignal.timeout(12000),
});
const text = await response.text();
const row = { provider, configuredStallSeconds: 1, upstreamDelaySeconds: 4.5, http: response.status,
seconds: Number(((performance.now() - start) / 1000).toFixed(3)), receivedMarker: text.includes("FIXTURE_OK"),
incompleteReason: text.match(/"reason":"([^"]+)"/)?.[1], raw: text };
results.push(row);
console.log(JSON.stringify({ ...row, raw: undefined }));
}
} finally {
await gateway.stop(true);
await upstream.stop(true);
writeFileSync(join(root, "stall-fixture-result.json"), JSON.stringify(results, null, 2));
}
Version
OpenCodex 2.97.0, Bun 1.4.4. Native delivery/inactivity source is unchanged from v2.97.0. A separate local hosted-tool compatibility patch is present but the fixture declares no tools and does not use that policy.
Operating system
macOS 27.0.1, Apple Silicon
Provider and model
Synthetic loopback providers only: fixture-native/probe (openai-responses) and fixture-chat/probe (openai-chat).
Logs or error output
{"provider":"fixture-native","configuredStallSeconds":1,"upstreamDelaySeconds":4.5,"http":200,"seconds":4.562,"receivedMarker":true}
{"provider":"fixture-chat","configuredStallSeconds":1,"upstreamDelaySeconds":4.5,"http":200,"seconds":1.040,"receivedMarker":false}
Translated route terminal:
{
"type": "response.failed",
"response": {
"status": "failed",
"error": {
"message": "Upstream response body stalled before completing",
"type": "upstream_error",
"code": "upstream_server_error"
}
}
}
Screenshots and supporting files
The fixture records its complete synthetic SSE transcripts and timing in stall-fixture-result.json.
Relevant source:
src/server/responses/passthrough-execution.tscallsguardDirectPassthroughBodyInactivityafter delivery.src/lib/response-body-inactivity.tsreturns an event-stream Response unchanged in that guard.- Native streaming relay ownership remains in
src/server/responses/passthrough-delivery.ts. - Documentation: https://github.com/lidge-jun/opencodex/blob/v2.97.0/docs-site/src/content/docs/reference/configuration/server.md
Related operational investigation: #8204 covers a different mixed-tool compatibility issue. A remote Gemini request ended without a terminal after roughly 304 seconds; a numerically similar configured stall value alone was insufficient evidence to assign that remote failure to the local setting, which prompted this fixture.
Redacted configuration
{
"stallTimeoutSec": 1,
"emptyCompletionRetry": false,
"clientIntegrations": {"codex": false, "grok": false, "claude-desktop": false},
"providers": {
"fixture-native": {
"adapter": "openai-responses",
"baseUrl": "http://127.0.0.1:<mock-port>/v1",
"apiKey": "fixture-key",
"allowPrivateNetwork": true,
"proxy": "direct",
"models": ["probe"]
},
"fixture-chat": {
"adapter": "openai-chat",
"baseUrl": "http://127.0.0.1:<same-mock-port>/v1",
"apiKey": "fixture-key",
"allowPrivateNetwork": true,
"proxy": "direct",
"models": ["probe"]
}
}
}
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
- Dominant language
- TypeScript
- Stars
- 16.9k
- Forks
- 1.3k
- Avg merge
- 19h 24m
- Merged PRs (30d)
- 676
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from lidge-jun/opencodex
-
account-pool bug gui
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
lidge-jun/opencodex#8131 · 3 comments ·
Maintainers usually reply within 1 day
-
bug streaming
Difficulty 4/5 3-5 days Newbie friendliness 48/100
lidge-jun/opencodex#8231 · 1 comment ·
Maintainers usually reply within 1 day
-
bug streaming
Difficulty 4/5 1-2 days Newbie friendliness 48/100
lidge-jun/opencodex#8216 · 1 comment ·
Maintainers usually reply within 1 day
-
provider provider-compatibility
Difficulty 4/5 3-5 days Newbie friendliness 35/100
lidge-jun/opencodex#8204 · 2 comments ·
Maintainers usually reply within 1 day
-
sub2api codex_cli_only rejects official Codex requests after client identifiers are strippedPossibly taken A pull request linked to this issue is open or already merged. Openprovider provider-compatibility streaming
Difficulty 3/5 1-2 days Newbie friendliness 55/100
lidge-jun/opencodex#8192 · 1 comment ·
Maintainers usually reply within 1 day
All issues in lidge-jun/opencodex
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
area:ui bug effort:S priority:P2
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Typo in WSL project description on Projects pagePossibly taken @akhil9tiet claimed this today. Open
Difficulty 1/5 Under an hour Newbie friendliness 75/100
microsoft/opensource.microsoft.com#1066 · 1 comment ·
-
ArcGIS Maps SDK for JavaScript c-carousel calcite-design-tokens enhancement impact - p3 - not time sensitive needs triage
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Esri/calcite-design-system#15565 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
linkwarden/linkwarden#1891 ·
Maintainers usually reply within 1 day