mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-19 16:39:15 +00:00
feat(provider): support per-source proxy_url for geo-blocked upstreams
Upstreams like justwoker/tabitoken sit behind Cloudflare geo/IP blocks and only respond through a proxy. A global env proxy is wrong (intranet sources trae/localzen must stay direct), so add an explicit per-source proxy_url that overrides http.ProxyFromEnvironment for just that source. Sources without proxy_url keep the existing env/direct behavior. Also accepts a User-Agent header per source (config already supported headers) so CF-fronted resellers can be reached. Verified e2e: tabitoken and justwoker now return tool_calls through 127.0.0.1:7890 (clash); trae stays direct. Full go test -tags luajit passes.
This commit is contained in:
@ -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"`
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user