mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
The configured request timeout (API_TIMEOUT_MS) only feeds the SDK client timeout, which on a streaming request is cleared the moment response headers arrive. Slow local/3P models return 200 headers fast but spend minutes prefilling before the first SSE token, so that window was governed by the idle watchdog (CLAUDE_STREAM_IDLE_TIMEOUT_MS, 240s) -- a separate env users cannot configure. The configured timeout therefore never applied to slow prefill, so raising it had no effect (#826, follow-up to #449). Split the stream watchdog into phases: use a first-token budget (tied to the user's request timeout via CLAUDE_STREAM_FIRST_TOKEN_TIMEOUT_MS) until the first chunk arrives, then switch to the shorter mid-stream idle budget. The overall max-duration cap (#766) still backstops trickling bodies; CLI defaults fall back to prior behavior.
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
import { SettingsService } from './settingsService.js'
|
|
|
|
export type NetworkProxyMode = 'system' | 'manual'
|
|
|
|
export type NetworkSettings = {
|
|
aiRequestTimeoutMs: number
|
|
proxy: {
|
|
mode: NetworkProxyMode
|
|
url: string
|
|
}
|
|
}
|
|
|
|
// aiRequestTimeoutMs is the user-facing "wait for the first reply" budget. It
|
|
// drives two CLI knobs in lockstep (see conversationService.buildChildEnv):
|
|
// 1. API_TIMEOUT_MS — the SDK client timeout, which on a streaming request
|
|
// only covers connection → response headers (the SDK clears it the moment
|
|
// headers arrive; the streaming body is not covered).
|
|
// 2. CLAUDE_STREAM_FIRST_TOKEN_TIMEOUT_MS — the CLI's first-token watchdog,
|
|
// which covers the gap between response headers and the FIRST SSE chunk.
|
|
// Together they make the configured timeout span the whole pre-first-token
|
|
// window: third-party gateways and local models (sensenova, bailian, zhipu,
|
|
// ollama, llama.cpp, ...) often send nothing — not headers, not an SSE ping —
|
|
// for minutes while prefilling a large context (#766, #826). Once tokens start
|
|
// flowing the CLI hands off to the shorter mid-stream idle watchdog. The
|
|
// default matches the SDK's own 600s.
|
|
export const DEFAULT_AI_REQUEST_TIMEOUT_MS = 600_000
|
|
export const MIN_AI_REQUEST_TIMEOUT_MS = 30_000
|
|
export const MAX_AI_REQUEST_TIMEOUT_MS = 1_800_000
|
|
|
|
const DEFAULT_NETWORK_SETTINGS: NetworkSettings = {
|
|
aiRequestTimeoutMs: DEFAULT_AI_REQUEST_TIMEOUT_MS,
|
|
proxy: {
|
|
mode: 'system',
|
|
url: '',
|
|
},
|
|
}
|
|
|
|
function isNetworkProxyMode(value: unknown): value is NetworkProxyMode {
|
|
return value === 'system' || value === 'manual'
|
|
}
|
|
|
|
function clampTimeoutMs(value: number): number {
|
|
return Math.min(Math.max(value, MIN_AI_REQUEST_TIMEOUT_MS), MAX_AI_REQUEST_TIMEOUT_MS)
|
|
}
|
|
|
|
function parseTimeoutMs(value: unknown): number {
|
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
return DEFAULT_NETWORK_SETTINGS.aiRequestTimeoutMs
|
|
}
|
|
return clampTimeoutMs(Math.round(value))
|
|
}
|
|
|
|
function parseProxy(value: unknown): NetworkSettings['proxy'] {
|
|
if (!value || typeof value !== 'object') {
|
|
return DEFAULT_NETWORK_SETTINGS.proxy
|
|
}
|
|
|
|
const record = value as Record<string, unknown>
|
|
return {
|
|
mode: isNetworkProxyMode(record.mode) ? record.mode : DEFAULT_NETWORK_SETTINGS.proxy.mode,
|
|
url: typeof record.url === 'string' ? record.url.trim() : '',
|
|
}
|
|
}
|
|
|
|
export function normalizeNetworkSettings(settings: unknown): NetworkSettings {
|
|
if (!settings || typeof settings !== 'object') {
|
|
return DEFAULT_NETWORK_SETTINGS
|
|
}
|
|
|
|
const record = settings as Record<string, unknown>
|
|
const rawNetwork = record.network
|
|
const network = rawNetwork && typeof rawNetwork === 'object'
|
|
? rawNetwork as Record<string, unknown>
|
|
: {}
|
|
|
|
return {
|
|
aiRequestTimeoutMs: parseTimeoutMs(network.aiRequestTimeoutMs),
|
|
proxy: parseProxy(network.proxy),
|
|
}
|
|
}
|
|
|
|
export function getManualNetworkProxyUrl(settings: NetworkSettings): string | undefined {
|
|
if (settings.proxy.mode !== 'manual') return undefined
|
|
const url = settings.proxy.url.trim()
|
|
return url || undefined
|
|
}
|
|
|
|
export function buildNetworkEnvironment(settings: NetworkSettings): Record<string, string> {
|
|
const env: Record<string, string> = {
|
|
API_TIMEOUT_MS: String(settings.aiRequestTimeoutMs),
|
|
}
|
|
const proxyUrl = getManualNetworkProxyUrl(settings)
|
|
|
|
if (proxyUrl) {
|
|
env.HTTP_PROXY = proxyUrl
|
|
env.HTTPS_PROXY = proxyUrl
|
|
env.http_proxy = proxyUrl
|
|
env.https_proxy = proxyUrl
|
|
}
|
|
|
|
return env
|
|
}
|
|
|
|
export async function loadNetworkSettings(): Promise<NetworkSettings> {
|
|
const settings = await new SettingsService().getUserSettings()
|
|
return normalizeNetworkSettings(settings)
|
|
}
|