mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 16:14:31 +00:00
fix(proxy): keep undici timeouts above the upstream request timeout (#1524)
Sets undici headersTimeout/bodyTimeout to request_timeout+30s so the AbortController is the single authority on upstream request lifetime, preventing premature socket closes on slow self-hosted upstreams. Verified undici v5 ProxyAgent object-signature compat.
This commit is contained in:
1 parent
0d856fc0ab
commit
8f9795bce2
5 files changed
+116
-13
No files matched your search
@@ -10,9 +10,16 @@ import {
|
||||
import { ProxySseStreamTransformer } from '../transformers/sse-stream-transformer';
|
||||
import { isAnthropicPassthroughProfile, resolveOpenAIChatCompletionsUrl } from '../upstream-url';
|
||||
import { createLogger } from '../../services/logging';
|
||||
import {
|
||||
createGlobalFetchProxyDispatcher,
|
||||
type UpstreamAgentTimeoutOptions,
|
||||
} from '../../utils/fetch-proxy-setup';
|
||||
import { pipeWebResponseToNode, readRawBody, writeJson } from './http-helpers';
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 600_000;
|
||||
// Keep undici's per-phase timeouts above the explicit request timeout so the
|
||||
// AbortController is the single authority on when an upstream request dies.
|
||||
const UPSTREAM_TIMEOUT_GRACE_MS = 30_000;
|
||||
const DIRECT_OPENAI_REASONING_CHAT_MODEL = /^(?:gpt-5|o[134])(?:[-.]|$)/;
|
||||
const logger = createLogger('proxy:openai-compat:messages');
|
||||
|
||||
@@ -375,7 +382,7 @@ function buildFetchInit(
|
||||
signal: AbortSignal,
|
||||
incomingHeaders: http.IncomingHttpHeaders,
|
||||
preserveUserAgent: boolean,
|
||||
insecureDispatcher?: Dispatcher
|
||||
dispatcher?: Dispatcher
|
||||
): RequestInit {
|
||||
const init: RequestInit = {
|
||||
method: 'POST',
|
||||
@@ -384,8 +391,8 @@ function buildFetchInit(
|
||||
signal,
|
||||
};
|
||||
|
||||
if (insecureDispatcher) {
|
||||
(init as Record<string, unknown>).dispatcher = insecureDispatcher;
|
||||
if (dispatcher) {
|
||||
(init as Record<string, unknown>).dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
return init;
|
||||
@@ -401,6 +408,30 @@ function getRequestTimeoutMs(): number {
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : REQUEST_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* undici defaults `headersTimeout`/`bodyTimeout` to 300s, which silently
|
||||
* undercuts {@link REQUEST_TIMEOUT_MS} (600s) and the
|
||||
* `CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS` override: slow upstreams (self-hosted
|
||||
* LLMs with long queue + prefill phases) get their socket closed at 300s with
|
||||
* a generic connection error instead of the proxy's timeout response. Every
|
||||
* dispatcher used for upstream fetches must carry these options.
|
||||
*/
|
||||
export function buildUpstreamAgentTimeouts(): UpstreamAgentTimeoutOptions {
|
||||
const ceiling = getRequestTimeoutMs() + UPSTREAM_TIMEOUT_GRACE_MS;
|
||||
return { headersTimeout: ceiling, bodyTimeout: ceiling };
|
||||
}
|
||||
|
||||
let defaultUpstreamDispatcher: Dispatcher | null = null;
|
||||
|
||||
function getDefaultUpstreamDispatcher(): Dispatcher {
|
||||
if (!defaultUpstreamDispatcher) {
|
||||
const timeouts = buildUpstreamAgentTimeouts();
|
||||
// Honor HTTP(S)_PROXY routing when configured; otherwise a plain Agent.
|
||||
defaultUpstreamDispatcher = createGlobalFetchProxyDispatcher(timeouts) ?? new Agent(timeouts);
|
||||
}
|
||||
return defaultUpstreamDispatcher;
|
||||
}
|
||||
|
||||
function formatTimeoutDuration(timeoutMs: number): string {
|
||||
return timeoutMs % 1000 === 0 ? `${timeoutMs / 1000} seconds` : `${timeoutMs}ms`;
|
||||
}
|
||||
@@ -538,19 +569,20 @@ export async function handleProxyMessagesRequest(
|
||||
const useProfileInsecureTls = upstream.route.profile.insecure === true;
|
||||
const ephemeralInsecureDispatcher =
|
||||
useProfileInsecureTls && !useSharedInsecureDispatcher
|
||||
? new Agent({ connect: { rejectUnauthorized: false } })
|
||||
? new Agent({ connect: { rejectUnauthorized: false }, ...buildUpstreamAgentTimeouts() })
|
||||
: undefined;
|
||||
const insecureTls = useSharedInsecureDispatcher || useProfileInsecureTls;
|
||||
const dispatcher = useSharedInsecureDispatcher
|
||||
? insecureDispatcher
|
||||
: useProfileInsecureTls
|
||||
? ephemeralInsecureDispatcher
|
||||
: undefined;
|
||||
: getDefaultUpstreamDispatcher();
|
||||
|
||||
try {
|
||||
logger.stage('dispatch', 'upstream.dispatch', 'Dispatching upstream fetch', {
|
||||
profileName: profile.profileName,
|
||||
routedProfileName: upstream.route.profile.profileName,
|
||||
insecureTls: dispatcher !== undefined,
|
||||
insecureTls,
|
||||
passthrough,
|
||||
});
|
||||
const upstreamUrl = resolveOpenAIChatCompletionsUrl(upstream.route.profile.baseUrl, {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { OpenAICompatProfileConfig } from '../profile-router';
|
||||
import { OPENAI_COMPAT_PROXY_SERVICE_NAME } from '../proxy-daemon-paths';
|
||||
import { createLogger, withRequestContext } from '../../services/logging';
|
||||
import {
|
||||
buildUpstreamAgentTimeouts,
|
||||
handleProxyMessagesRequest,
|
||||
handleProxyModelsRequest,
|
||||
validateIncomingProxyAuth,
|
||||
@@ -39,7 +40,7 @@ export function startOpenAICompatProxyServer(options: OpenAICompatProxyServerOpt
|
||||
port: options.port,
|
||||
});
|
||||
const insecureDispatcher = options.insecure
|
||||
? new Agent({ connect: { rejectUnauthorized: false } })
|
||||
? new Agent({ connect: { rejectUnauthorized: false }, ...buildUpstreamAgentTimeouts() })
|
||||
: undefined;
|
||||
const server = http.createServer((req, res) => {
|
||||
const requestId = resolveInboundRequestId(req.headers);
|
||||
|
||||
Reference in new issue
Block a user