[Bug] Explicit stallTimeoutSec is not enforced for native Responses SSE while translated Chat honors it

Open
#8,206 1 comment 0 reactions 0 assignees View on GitHub

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

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

bug streaming
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.failed at 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:

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from lidge-jun/opencodex

All issues in lidge-jun/opencodex

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.