feat(sse): per-model upstream header-response timeout override (#6354) (#7632)

Validated in local merge-train @ 8f27177d1 (full parity suite green: typecheck+file-size+complexity+cognitive+changelog+unit shards 1&2+vitest)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-17 22:09:14 -03:00
committed by GitHub
parent 9b3ad09b38
commit c71eeae4ad
6 changed files with 109 additions and 2 deletions

View File

@@ -0,0 +1 @@
- feat(sse): configurable per-model upstream header-response timeout override, precedence model > provider > global; applied to codex reasoning-heavy tiers (gpt-5.5-high/xhigh, gpt-5.6-\*-high/xhigh) (#6354)

View File

@@ -73,6 +73,21 @@ export function getModelsByProviderId(providerId: string): RegistryModel[] {
return PROVIDER_MODELS[alias] || [];
}
/**
* Model-level upstream header-response timeout override, when the registry
* entry for `modelId` sets one (#6354). Returns `undefined` when the model
* isn't found or has no override, so callers can fall through to the
* provider-level/global defaults unchanged.
*/
export function getModelTimeoutMs(aliasOrId: string, modelId: string): number | undefined {
// Callers (e.g. chatCore's timeout resolution) pass the raw provider id
// ("codex"), not the public alias ("cx") that PROVIDER_MODELS is keyed by
// — resolve id→alias the same way getProviderModels()/getModelsByProviderId()
// do, so the override actually resolves (#6354).
const alias = PROVIDER_ID_TO_ALIAS[aliasOrId] || aliasOrId;
return getProviderModel(alias, modelId)?.timeoutMs;
}
const CLAUDE_MODEL_PATTERN = /(?:^|[\/._-])claude(?:[._-]|$)/;
const CLAUDE_MAX_EFFORT_UNSUPPORTED_FAMILY_PATTERNS = [/(?:^|[\/._-])haiku(?:[._-]|$)/] as const;
const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-";

View File

@@ -43,11 +43,15 @@ export const codexProvider: RegistryEntry = {
id: "gpt-5.6-sol-xhigh",
name: "GPT 5.6 Sol (xHigh)",
...GPT_5_6_CODEX_CAPABILITIES,
// #6354: reasoning-heavy tier — more header-wait room than the global default.
timeoutMs: 1200000,
},
{
id: "gpt-5.6-sol-high",
name: "GPT 5.6 Sol (High)",
...GPT_5_6_CODEX_CAPABILITIES,
// #6354: reasoning-heavy tier — more header-wait room than the global default.
timeoutMs: 1200000,
},
{
id: "gpt-5.6-sol-medium",
@@ -78,11 +82,15 @@ export const codexProvider: RegistryEntry = {
id: "gpt-5.6-terra-xhigh",
name: "GPT 5.6 Terra (xHigh)",
...GPT_5_6_CODEX_CAPABILITIES,
// #6354: reasoning-heavy tier — more header-wait room than the global default.
timeoutMs: 1200000,
},
{
id: "gpt-5.6-terra-high",
name: "GPT 5.6 Terra (High)",
...GPT_5_6_CODEX_CAPABILITIES,
// #6354: reasoning-heavy tier — more header-wait room than the global default.
timeoutMs: 1200000,
},
{
id: "gpt-5.6-terra-medium",
@@ -108,11 +116,15 @@ export const codexProvider: RegistryEntry = {
id: "gpt-5.6-luna-xhigh",
name: "GPT 5.6 Luna (xHigh)",
...GPT_5_6_CODEX_CAPABILITIES,
// #6354: reasoning-heavy tier — more header-wait room than the global default.
timeoutMs: 1200000,
},
{
id: "gpt-5.6-luna-high",
name: "GPT 5.6 Luna (High)",
...GPT_5_6_CODEX_CAPABILITIES,
// #6354: reasoning-heavy tier — more header-wait room than the global default.
timeoutMs: 1200000,
},
{
id: "gpt-5.6-luna-medium",
@@ -149,6 +161,8 @@ export const codexProvider: RegistryEntry = {
// #6191: input cap per reporter; TODO confirm exact value
maxInputTokens: 272000,
maxOutputTokens: 128000,
// #6354: reasoning-heavy tier — more header-wait room than the global default.
timeoutMs: 1200000,
},
{
id: "gpt-5.5-high",
@@ -158,6 +172,8 @@ export const codexProvider: RegistryEntry = {
// #6191: input cap per reporter; TODO confirm exact value
maxInputTokens: 272000,
maxOutputTokens: 128000,
// #6354: reasoning-heavy tier — more header-wait room than the global default.
timeoutMs: 1200000,
},
{
id: "gpt-5.5-medium",

View File

@@ -71,6 +71,9 @@ export interface RegistryModel {
* reasoning_content instead of failing with a DeepSeek 400 (#2900).
*/
interleavedField?: string;
/** Per-model upstream header-response timeout override — precedes
* `RegistryEntry.timeoutMs` and the global `FETCH_TIMEOUT_MS` (#6354). */
timeoutMs?: number;
}
// Reasoning models reject temperature, top_p, penalties, logprobs, n.

View File

@@ -1,4 +1,5 @@
import { FETCH_TIMEOUT_MS } from "../../config/constants.ts";
import { getModelTimeoutMs } from "../../config/providerModels.ts";
import {
getLoggedInputTokens,
getLoggedOutputTokens,
@@ -62,7 +63,16 @@ export function computeBillableTokens(usage: unknown): number {
return getLoggedInputTokens(usage) + getLoggedOutputTokens(usage) + getReasoningTokens(usage);
}
export function getExecutorTimeoutMs(executor: unknown): number {
/** Resolves the model-level `timeoutMs` registry override, when both
* `provider` and `model` are known and the model registers one (#6354). */
function resolveModelTimeoutOverride(provider?: string, model?: string): number | undefined {
if (!provider || !model) return undefined;
const override = getModelTimeoutMs(provider, model);
if (typeof override !== "number" || !Number.isFinite(override)) return undefined;
return Math.max(0, Math.floor(override));
}
function resolveProviderTimeoutMs(executor: unknown): number {
const getTimeoutMs = (executor as { getTimeoutMs?: () => unknown } | null)?.getTimeoutMs;
if (typeof getTimeoutMs !== "function") return FETCH_TIMEOUT_MS;
@@ -75,6 +85,19 @@ export function getExecutorTimeoutMs(executor: unknown): number {
}
}
/**
* Resolves the upstream header-response timeout in precedence order:
* model-level override (registry `RegistryModel.timeoutMs`) → provider-level
* override (`executor.getTimeoutMs()`) → global `FETCH_TIMEOUT_MS` default.
* `provider`/`model` are optional so existing single-argument call sites
* keep resolving to the provider/global chain unchanged (#6354).
*/
export function getExecutorTimeoutMs(executor: unknown, provider?: string, model?: string): number {
const modelOverride = resolveModelTimeoutOverride(provider, model);
if (modelOverride !== undefined) return modelOverride;
return resolveProviderTimeoutMs(executor);
}
export function normalizeExecutorResult(
result:
| Response
@@ -111,7 +134,7 @@ export async function executeWithUpstreamStartTimeout<T>({
log?: { warn?: (tag: string, message: string) => void } | null;
execute: (signal: AbortSignal) => Promise<T>;
}): Promise<T> {
const timeoutMs = getExecutorTimeoutMs(executor);
const timeoutMs = getExecutorTimeoutMs(executor, provider, model);
if (timeoutMs <= 0) return execute(signal);
if (signal.aborted) throw createAbortError(signal);

View File

@@ -0,0 +1,49 @@
import test from "node:test";
import assert from "node:assert/strict";
import { getExecutorTimeoutMs } from "../../open-sse/handlers/chatCore/upstreamTimeouts.ts";
import { FETCH_TIMEOUT_MS } from "../../open-sse/config/constants.ts";
import { getModelTimeoutMs } from "../../open-sse/config/providerModels.ts";
test("model-level timeoutMs wins over provider-level and global", () => {
const executor = { getTimeoutMs: () => 30000 };
const result = getExecutorTimeoutMs(executor, "codex", "gpt-5.5-high");
assert.equal(result, 1200000);
assert.notEqual(result, 30000);
assert.notEqual(result, FETCH_TIMEOUT_MS);
});
test("provider-level timeoutMs still wins over global when no model override exists (regression guard)", () => {
const executor = { getTimeoutMs: () => 45000 };
// No model registered for this provider/model combo -> no model override,
// must still fall back to the provider-level executor.getTimeoutMs().
const result = getExecutorTimeoutMs(executor, "codex", "gpt-5.5-medium");
assert.equal(result, 45000);
});
test("global FETCH_TIMEOUT_MS remains the fallback with neither override", () => {
const executor = {};
const result = getExecutorTimeoutMs(executor, "codex", "gpt-5.5-medium");
assert.equal(result, FETCH_TIMEOUT_MS);
});
test("getExecutorTimeoutMs is backward compatible when called with only an executor", () => {
const executor = { getTimeoutMs: () => 5000 };
assert.equal(getExecutorTimeoutMs(executor), 5000);
});
test("gpt-5.5-high resolves to its override; gpt-5.5-medium resolves to the provider/global default (reported-scenario guard)", () => {
const executor = {};
const highResult = getExecutorTimeoutMs(executor, "codex", "gpt-5.5-high");
const mediumResult = getExecutorTimeoutMs(executor, "codex", "gpt-5.5-medium");
assert.equal(highResult, 1200000);
assert.equal(mediumResult, FETCH_TIMEOUT_MS);
assert.notEqual(highResult, mediumResult);
});
test("getModelTimeoutMs returns the registered override for reasoning-heavy codex tiers", () => {
assert.equal(getModelTimeoutMs("codex", "gpt-5.5-high"), 1200000);
assert.equal(getModelTimeoutMs("codex", "gpt-5.5-xhigh"), 1200000);
assert.equal(getModelTimeoutMs("codex", "gpt-5.5-medium"), undefined);
assert.equal(getModelTimeoutMs("codex", "nonexistent-model"), undefined);
});