diff --git a/.env.example b/.env.example index 844cb0659a..d356c7a123 100644 --- a/.env.example +++ b/.env.example @@ -449,6 +449,7 @@ GEMINI_CLI_USER_AGENT=google-api-nodejs-client/9.15.1 # ── Upstream fetch (provider calls) ── # FETCH_TIMEOUT_MS=600000 # Total request timeout (default: 600000 = 10 min) +# # Also drives anthropic-compatible-cc-* X-Stainless-Timeout. # FETCH_HEADERS_TIMEOUT_MS=600000 # Time to receive response headers # FETCH_BODY_TIMEOUT_MS=600000 # Time to receive full response body # FETCH_CONNECT_TIMEOUT_MS=30000 # TCP connection establishment (default: 30s) diff --git a/README.md b/README.md index a6d9876fa1..e4411783c1 100644 --- a/README.md +++ b/README.md @@ -799,6 +799,8 @@ For most deployments, you only need: Backward compatibility is preserved: existing `FETCH_TIMEOUT_MS`, `API_BRIDGE_PROXY_TIMEOUT_MS`, and other per-layer timeout vars still work and override the shared baseline. +For Claude Code-compatible upstreams (`anthropic-compatible-cc-*`), OmniRoute also derives the outbound `X-Stainless-Timeout` header from the resolved fetch timeout so provider-side read timeouts stay aligned with your env configuration. + Advanced overrides are available if you need finer control: | Variable | Default | Purpose | diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 0294bf3199..3c5b8c3a4e 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -1,5 +1,6 @@ import { createHash, randomUUID } from "node:crypto"; +import { getStainlessTimeoutSeconds } from "@/shared/utils/runtimeTimeouts"; import { prepareClaudeRequest } from "../translator/helpers/claudeHelper.ts"; export const CLAUDE_CODE_COMPATIBLE_PREFIX = "anthropic-compatible-cc-"; @@ -12,6 +13,9 @@ export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA = export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.89 (external, sdk-cli)"; export const CLAUDE_CODE_COMPATIBLE_BILLING_HEADER = "x-anthropic-billing-header: cc_version=2.1.89.728; cc_entrypoint=sdk-cli; cch=00000;"; +export const CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS = getStainlessTimeoutSeconds( + process.env +); type HeaderLike = | Headers @@ -97,7 +101,7 @@ export function buildClaudeCodeCompatibleHeaders( "x-app": "cli", "User-Agent": CLAUDE_CODE_COMPATIBLE_USER_AGENT, "X-Stainless-Retry-Count": "0", - "X-Stainless-Timeout": "300", + "X-Stainless-Timeout": String(CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS), "X-Stainless-Lang": "js", "X-Stainless-Package-Version": "0.74.0", "X-Stainless-OS": "MacOS", diff --git a/src/shared/utils/runtimeTimeouts.ts b/src/shared/utils/runtimeTimeouts.ts index 9d5e39797b..b2794a7745 100644 --- a/src/shared/utils/runtimeTimeouts.ts +++ b/src/shared/utils/runtimeTimeouts.ts @@ -121,6 +121,14 @@ export function getUpstreamTimeoutConfig( }; } +export function getStainlessTimeoutSeconds( + env: EnvSource = process.env, + logger?: TimeoutLogger +): number { + const { fetchTimeoutMs } = getUpstreamTimeoutConfig(env, logger); + return Math.max(1, Math.ceil(fetchTimeoutMs / 1_000)); +} + export function getTlsClientTimeoutConfig( env: EnvSource = process.env, logger?: TimeoutLogger diff --git a/tests/unit/claude-code-compatible-helpers.test.mjs b/tests/unit/claude-code-compatible-helpers.test.mjs index 29732d6bf2..ef721f28f9 100644 --- a/tests/unit/claude-code-compatible-helpers.test.mjs +++ b/tests/unit/claude-code-compatible-helpers.test.mjs @@ -5,6 +5,7 @@ const { CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH, CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH, CLAUDE_CODE_COMPATIBLE_DEFAULT_MAX_TOKENS, + CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS, isClaudeCodeCompatibleProvider, stripAnthropicMessagesSuffix, stripClaudeCodeCompatibleEndpointSuffix, @@ -46,6 +47,10 @@ test("buildClaudeCodeCompatibleHeaders emits stream-aware auth headers and sessi assert.equal(streamHeaders.Accept, "text/event-stream"); assert.equal(streamHeaders["x-api-key"], "sk-demo"); assert.equal(streamHeaders["X-Claude-Code-Session-Id"], "session-123"); + assert.equal( + streamHeaders["X-Stainless-Timeout"], + String(CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS) + ); assert.equal(jsonHeaders.Accept, "application/json"); assert.equal(jsonHeaders["X-Claude-Code-Session-Id"], undefined); }); diff --git a/tests/unit/runtime-timeouts.test.mjs b/tests/unit/runtime-timeouts.test.mjs index 8a1c5e6ce7..a1db61da52 100644 --- a/tests/unit/runtime-timeouts.test.mjs +++ b/tests/unit/runtime-timeouts.test.mjs @@ -65,6 +65,21 @@ test("TLS client timeout defaults to FETCH_TIMEOUT_MS and can be overridden", () assert.equal(overriddenConfig.timeoutMs, 720000); }); +test("stainless timeout derives from fetch timeout and rounds up to whole seconds", () => { + assert.equal( + runtimeTimeouts.getStainlessTimeoutSeconds({ + REQUEST_TIMEOUT_MS: "1200000", + }), + 1200 + ); + assert.equal( + runtimeTimeouts.getStainlessTimeoutSeconds({ + FETCH_TIMEOUT_MS: "600001", + }), + 601 + ); +}); + test("API bridge timeouts align request timeout with long proxy timeout by default", () => { const config = runtimeTimeouts.getApiBridgeTimeoutConfig({ API_BRIDGE_PROXY_TIMEOUT_MS: "600000",