mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 16:15:24 +00:00
103 lines
4.5 KiB
Go
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()}
|
|
}
|