diff --git a/internal/config/config.go b/internal/config/config.go index 87be1c7..7fbdfe4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -47,15 +47,19 @@ type Model struct { // Source describes a single upstream LLM provider. type Source struct { - Name string `yaml:"name" json:"name"` - BaseURL string `yaml:"base_url" json:"base_url"` - APIKey string `yaml:"api_key" json:"api_key"` - APIKeyEnv string `yaml:"api_key_env,omitempty" json:"-"` // reference to an env var holding the key (overrides api_key) - Adapter string `yaml:"adapter" json:"adapter"` - Endpoint string `yaml:"endpoint" json:"endpoint,omitempty"` // chat endpoint override - ImageEndpoint string `yaml:"image_endpoint" json:"image_endpoint,omitempty"` // image endpoint override - Models []Model `yaml:"models" json:"models"` - Headers map[string]string `yaml:"headers" json:"headers,omitempty"` + Name string `yaml:"name" json:"name"` + BaseURL string `yaml:"base_url" json:"base_url"` + APIKey string `yaml:"api_key" json:"api_key"` + APIKeyEnv string `yaml:"api_key_env,omitempty" json:"-"` // reference to an env var holding the key (overrides api_key) + Adapter string `yaml:"adapter" json:"adapter"` + Endpoint string `yaml:"endpoint" json:"endpoint,omitempty"` // chat endpoint override + ImageEndpoint string `yaml:"image_endpoint" json:"image_endpoint,omitempty"` // image endpoint override + Models []Model `yaml:"models" json:"models"` + Headers map[string]string `yaml:"headers" json:"headers,omitempty"` + // ProxyURL routes this source's HTTP(S) traffic through an explicit proxy + // (e.g. http://127.0.0.1:7890). Empty = direct connection. Needed for + // upstreams behind geo/IP blocks that only respond through a proxy. + ProxyURL string `yaml:"proxy_url" json:"proxy_url,omitempty"` Meta map[string]interface{} `yaml:"meta" json:"meta,omitempty"` Temperature float64 `yaml:"temperature" json:"temperature,omitempty"` MaxTokens int `yaml:"max_tokens" json:"max_tokens,omitempty"` @@ -328,13 +332,15 @@ type RuntimeConfig struct { // name and api_key) so the WebUI can spin up multiple key-bearing sources // from one shared template. type SourceTemplate struct { - Name string `json:"name"` - BaseURL string `json:"base_url"` - Adapter string `json:"adapter"` - Endpoint string `json:"endpoint,omitempty"` - ImageEndpoint string `json:"image_endpoint,omitempty"` - Models []Model `json:"models"` - Headers map[string]string `json:"headers,omitempty"` + Name string `json:"name"` + BaseURL string `json:"base_url"` + Adapter string `json:"adapter"` + Endpoint string `json:"endpoint,omitempty"` + ImageEndpoint string `json:"image_endpoint,omitempty"` + Models []Model `json:"models"` + Headers map[string]string `json:"headers,omitempty"` + // ProxyURL routes this source's HTTP(S) traffic through an explicit proxy. + ProxyURL string `json:"proxy_url,omitempty"` Meta map[string]interface{} `json:"meta,omitempty"` Temperature float64 `json:"temperature,omitempty"` MaxTokens int `json:"max_tokens,omitempty"` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 46c48b3..c942199 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -13,6 +13,7 @@ import ( "log" "net" "net/http" + "net/url" "strconv" "strings" "sync" @@ -24,6 +25,20 @@ import ( "llmsproxy/internal/types" ) +// proxyFor picks the transport proxy for one source. An explicit per-source +// proxy_url wins (e.g. clash on 127.0.0.1:7890 for a geo-blocked upstream); +// otherwise fall back to the process-wide env proxy, which is direct by +// default. +func proxyFor(cfg config.Source) func(*http.Request) (*url.URL, error) { + if cfg.ProxyURL != "" { + proxyURL, err := url.Parse(cfg.ProxyURL) + if err == nil { + return http.ProxyURL(proxyURL) + } + } + return http.ProxyFromEnvironment +} + // ---- per-(source,model) scheduling state ---- // ModelState is the scheduling state of one (source, model) pair: a soft @@ -388,8 +403,11 @@ func New(cfg config.Source, vm *lua.VM) *Provider { // Shared transport: ResponseHeaderTimeout bounds how long we wait for the // first response byte (applies to both paths); the stream client has no // client-level Timeout so the SSE body can run past the header timeout. + // A per-source ProxyURL (e.g. clash on 127.0.0.1:7890) overrides the + // process-wide env proxy for upstreams that are geo/IP-blocked; sources + // without one keep http.ProxyFromEnvironment (direct by default). tr := &http.Transport{ - Proxy: http.ProxyFromEnvironment, + Proxy: proxyFor(cfg), DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100,