feat(executors): make chatgpt-session stream-open timeout tunable

Add OMNIROUTE_CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS to override the
30s CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS default, mirroring
resolveDirectHeadersTimeoutMs's validation. Resolved per request in
the executor so a changed env var takes effect without a restart.
This commit is contained in:
Markus Hartung
2026-09-02 15:28:38 -03:00
parent d9041b7070
commit 6850e93002
5 changed files with 81 additions and 1 deletions

View File

@@ -2922,6 +2922,15 @@ QUOTA_STORE_DRIVER=sqlite
# CODEX_CHATGPT_WEB_BUN=/absolute/path/to/bun
# CODEX_WEB_GPT_BUN=/absolute/path/to/bun
# ─────────────────────────────────────────────────────────────────────────────
# ChatGPT Session provider — stream-open gate
# Used by: open-sse/executors/chatgpt-session/bridge.ts
# How long the stream-open gate waits for the first committing event before
# opening the stream anyway. Falls back to the 30s default when unset, empty,
# unparseable, non-finite, or not positive.
# ─────────────────────────────────────────────────────────────────────────────
# OMNIROUTE_CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS=30000
# ─────────────────────────────────────────────────────────────────────────────
# Browser-login VNC sessions (optional — src/lib/vncSession/manifest.ts)
# Containerized Chromium+VNC used for interactive browser-login credential

View File

@@ -749,6 +749,7 @@ REQUEST_TIMEOUT_MS (global override)
| `OMNIROUTE_CODEX_APPSERVER_AUTO_APPROVE` | `false` | Auto-approve the app-server's own approval prompts (command/file/permission execution on the host). Off by default — prompts are auto-denied; harness tool calls are unaffected (they travel the separate `item/tool/call` passthrough). Accepts `true`/`1`/`yes`. Per-connection override: `providerSpecificData.codexAppServerAutoApprove`. |
| `FETCH_HEADERS_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | Time to receive response headers. |
| `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS` | `30000` (30s) | Maximum response-start wait (ms) for each direct no-proxy attempt. A timeout retries once on a fresh socket; set `0` to disable the bound and retain the previous behavior. |
| `OMNIROUTE_CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS` | `30000` (30s) | How long the `chatgpt-session` executor's stream-open gate waits for the first committing event before opening the stream anyway (`open-sse/executors/chatgpt-session/bridge.ts`). Falls back to the 30s default when unset, empty, unparseable, non-finite, or not positive. |
| `FETCH_BODY_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | Time to receive the full response body. |
| `FETCH_CONNECT_TIMEOUT_MS` | `30000` | TCP connection establishment timeout. |
| `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Keep-alive socket idle timeout. |

View File

@@ -24,6 +24,7 @@ import { connectionRuntimePaths } from "./chatgpt-web-codex/storageState.ts";
import {
buildChatGptSessionCompletion,
openChatGptSessionStream,
resolveChatGptSessionStreamOpenTimeoutMs,
type ChatGptSessionResponseMeta,
} from "./chatgpt-session/bridge.ts";
import { classifyChatGptSessionError } from "./chatgpt-session/errors.ts";
@@ -311,7 +312,9 @@ export class ChatGptSessionExecutor extends BaseExecutor {
}
void run();
const opened = await openChatGptSessionStream(events, meta);
const opened = await openChatGptSessionStream(events, meta, {
streamOpenTimeoutMs: resolveChatGptSessionStreamOpenTimeoutMs(),
});
if (opened.kind === "error") {
// Every field of the verdict comes from the bridge's classification of the real adapter
// event — status, code and fallbackHint alike. Re-classifying the sanitized message here

View File

@@ -64,6 +64,24 @@ const COMMENTARY_PHASE: CodexMessagePhase = "commentary";
*/
export const CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS = 30_000;
/**
* Reads `OMNIROUTE_CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS` and falls back to
* {@link CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS} whenever the variable is unset, empty,
* unparseable, non-finite, or not positive — mirrors `resolveDirectHeadersTimeoutMs`
* (`open-sse/utils/directResponseStartTimeout.ts`). Callers resolve this per request so a
* changed environment variable takes effect without a restart.
*/
export function resolveChatGptSessionStreamOpenTimeoutMs(
env: Record<string, string | undefined> = process.env
): number {
const raw = env.OMNIROUTE_CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS;
if (raw == null || raw.trim() === "") return CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS;
const parsed = Number(raw);
return Number.isFinite(parsed) && parsed > 0
? Math.floor(parsed)
: CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS;
}
/** Race marker for the gate deadline — distinguishable from any `IteratorResult`. */
const GATE_TIMED_OUT = Symbol("chatgpt-session-gate-timeout");

View File

@@ -5,6 +5,7 @@ import {
CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS,
buildChatGptSessionCompletion,
openChatGptSessionStream,
resolveChatGptSessionStreamOpenTimeoutMs,
} from "../../open-sse/executors/chatgpt-session/bridge.ts";
import type { AdapterEvent } from "../../open-sse/vendor/codex-chatgpt-web/types.ts";
@@ -438,3 +439,51 @@ test("no event is lost to the deadline race", async () => {
assert.match(text, /"content":"one"/);
assert.match(text, /"content":"two"/);
});
const STREAM_OPEN_TIMEOUT_ENV_VAR = "OMNIROUTE_CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS";
function withStreamOpenTimeoutEnv(raw: string | undefined, run: () => void): void {
const saved = process.env[STREAM_OPEN_TIMEOUT_ENV_VAR];
if (raw === undefined) delete process.env[STREAM_OPEN_TIMEOUT_ENV_VAR];
else process.env[STREAM_OPEN_TIMEOUT_ENV_VAR] = raw;
try {
run();
} finally {
if (saved === undefined) delete process.env[STREAM_OPEN_TIMEOUT_ENV_VAR];
else process.env[STREAM_OPEN_TIMEOUT_ENV_VAR] = saved;
}
}
test("resolveChatGptSessionStreamOpenTimeoutMs falls back to the default when the env var is unset", () => {
withStreamOpenTimeoutEnv(undefined, () => {
assert.equal(
resolveChatGptSessionStreamOpenTimeoutMs(),
CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS
);
});
});
test("resolveChatGptSessionStreamOpenTimeoutMs honors a valid positive integer", () => {
withStreamOpenTimeoutEnv("45000", () => {
assert.equal(resolveChatGptSessionStreamOpenTimeoutMs(), 45_000);
});
});
const INVALID_STREAM_OPEN_TIMEOUT_INPUTS: Array<[label: string, raw: string]> = [
["an empty string", ""],
["non-numeric text", "not-a-number"],
["zero", "0"],
["a negative number", "-10"],
["a non-finite value", "Infinity"],
];
for (const [label, raw] of INVALID_STREAM_OPEN_TIMEOUT_INPUTS) {
test(`resolveChatGptSessionStreamOpenTimeoutMs falls back to the default for ${label}`, () => {
withStreamOpenTimeoutEnv(raw, () => {
assert.equal(
resolveChatGptSessionStreamOpenTimeoutMs(),
CHATGPT_SESSION_STREAM_OPEN_TIMEOUT_MS
);
});
});
}