Files
Clark Cant 9f719b023a Merge pull request #1464 from connermo/feat/outbound-dispatch-sharding
perf(channels): shard outbound dispatch per conversation
2026-08-01 04:32:20 +07:00

103 lines
4.5 KiB
Go

package providers
import (
"net/http"
"os"
"strconv"
"time"
)
// Provider-level defaults for HTTP clients and stream parsing.
const (
// Deprecated: DefaultHTTPTimeout set a wall-clock socket timeout that prevented
// ctx cancellation from unblocking bufio.Scanner. Use NewDefaultHTTPClient() instead.
DefaultHTTPTimeout = 300 * time.Second
// SSE stream reader initial buffer size (OpenAI-compat, Anthropic, Codex).
// No max: bufio.Reader.ReadString grows to fit a line of any length — a full
// base64 image in a single image-generation SSE data line can exceed several MB.
SSEScanBufInit = 64 * 1024 // 64KB initial buffer
// Stdio/JSONRPC scanner buffer sizes (Claude CLI, ACP).
StdioScanBufInit = 256 * 1024 // 256KB initial buffer
StdioScanBufMax = 10 * 1024 * 1024 // 10MB max for large protocol messages
)
// Idle connection pooling, overridable for deployments that raise agent-run
// concurrency:
//
// GOCLAW_HTTP_MAX_IDLE_CONNS=100
// GOCLAW_HTTP_MAX_IDLE_CONNS_PER_HOST=10
//
// When nearly all traffic goes to one provider host, the per-host limit is the
// one that binds: every concurrent request past it gets a fresh TCP+TLS
// handshake, and its connection is closed rather than pooled on completion.
// A self-hosted or in-network provider has no reason to be throttled this way,
// so those deployments should raise it to match LaneMain.
const (
defaultMaxIdleConns = 100
defaultMaxIdleConnsPerHost = 10
)
// transportEnv reads a positive int from an env var, falling back to defaultVal.
func transportEnv(key string, defaultVal int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
return n
}
}
return defaultVal
}
// NewDefaultTransport returns an http.Transport with per-stage timeouts but no
// overall deadline. The absence of Client.Timeout allows LLM streaming responses
// (extended thinking, long completions) to run indefinitely once the response has
// STARTED, while ctx cancellation still terminates the request promptly via
// CtxBody.
//
// Note that ctx cancellation is a user/caller signal here, not a time bound:
// agent runs get their context from scheduler queue.go via context.WithCancel,
// so no deadline is attached. The stage timeouts below are therefore the only
// thing bounding a request that never answers.
func NewDefaultTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
// Wait for the first byte of the response. This bounds a SINGLE attempt;
// RetryDo wraps the call with Attempts: 3, so the worst case a caller
// observes is ~3x this value, and a workflow step that requeues 3 times
// multiplies it again. Keep it modest.
//
// Raised from 180s after live evidence 2026-07-28: a reasoning model
// (claude-opus-5-thinking) emits nothing at all while thinking, so with a
// ~110k-token prompt it routinely passed 3 minutes before its first byte.
// GoClaw killed the connection and reported "http2: timeout awaiting
// response headers" while 9router's own usage log showed the upstream
// answering fine moments later (promptTokens=77194 → completionTokens=2789).
// The request was healthy; only this deadline was too short.
//
// Deliberately NOT removed: nothing else bounds an LLM call. The scheduler
// builds run contexts with context.WithCancel, not WithTimeout, so there is
// no run-level deadline to fall back on — despite what this file's older
// comment claimed. Without this timeout a silently dead connection would
// hold its scheduler lane until the OS TCP keepalive gave up (~2h), and
// requeue could not rescue it because requeue only fires when a run ends.
ResponseHeaderTimeout: 300 * time.Second,
IdleConnTimeout: 90 * time.Second, // close idle keep-alive connections
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: transportEnv("GOCLAW_HTTP_MAX_IDLE_CONNS", defaultMaxIdleConns),
MaxIdleConnsPerHost: transportEnv("GOCLAW_HTTP_MAX_IDLE_CONNS_PER_HOST", defaultMaxIdleConnsPerHost),
}
}
// NewDefaultHTTPClient returns an *http.Client backed by NewDefaultTransport.
// No Client.Timeout is set — rely on ctx deadlines and Transport stage timeouts.
//
// SSRF protection for user-configured provider URLs is enforced at provider
// create/update time by validateProviderURL (resolves the host and rejects
// private/reserved IPs via security.IsBlocked). Dial-time DNS-rebinding
// hardening is tracked as a follow-up.
func NewDefaultHTTPClient() *http.Client {
return &http.Client{Transport: NewDefaultTransport()}
}