mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 17:07:59 +00:00
fix: close P10 audit items — P10-1 sources API admin guard, P10-2 scope prefix strip, P10-3 runtime source timeout w/ stream-safe clients
- api.go: handleSourcesAPI now requires admin role (GET leaks upstream api_keys, POST/DELETE mutate routing)
- chat.go: hasScopeModel made a Gateway method that strips source-model/:// prefix strictly via Registry.EffectiveModel (only when the prefix names a real source serving the bare model) so dash-bearing ids like deepseek-v4-flash-free are never corrupted; +TestHasScopeModelWithSourcePrefix
- config.go: DefaultSourceTimeout/QueueTimeout/Concurrency constants shared by YAML ApplyDefaults and runtime sources
- core.go: mergedSources applies the same defaults to runtime sources (JSON never persisted timeout fields); a dead upstream can no longer hold a concurrency slot forever
- provider.go: split non-streaming client{Timeout} vs stream client{} sharing a Transport with ResponseHeaderTimeout, so long SSE bodies are not cut by client.Timeout; ChatStream uses doRawStream
- plan.md: mark P4-4/5/6 done, record P4-7/8 (tier-order, audit export, UI key view, zen upstream diagnosis)
- online verified: user key -> /api/sources 403 (GET+POST), admin 200, AUTO stream/non-stream healthy
This commit is contained in:
@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -129,7 +130,11 @@ type Provider struct {
|
||||
cfg config.Source
|
||||
vm *lua.VM
|
||||
adapter string
|
||||
client *http.Client
|
||||
// client bounds a whole non-streaming request (dial+read body). stream
|
||||
// is used for SSE: no overall timeout (a long stream must not be cut),
|
||||
// only the transport's ResponseHeaderTimeout bounds time-to-first-byte.
|
||||
client *http.Client
|
||||
stream *http.Client
|
||||
|
||||
mu sync.Mutex
|
||||
sem chan struct{}
|
||||
@ -142,11 +147,31 @@ type Provider struct {
|
||||
}
|
||||
|
||||
func New(cfg config.Source, vm *lua.VM) *Provider {
|
||||
if cfg.Timeout <= 0 {
|
||||
cfg.Timeout = config.DefaultSourceTimeout
|
||||
}
|
||||
if cfg.MaxConcurrent <= 0 {
|
||||
cfg.MaxConcurrent = config.DefaultSourceConcurrency
|
||||
}
|
||||
// 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.
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: cfg.Timeout,
|
||||
}
|
||||
p := &Provider{
|
||||
cfg: cfg,
|
||||
vm: vm,
|
||||
adapter: cfg.Adapter,
|
||||
client: &http.Client{Timeout: cfg.Timeout},
|
||||
client: &http.Client{Timeout: cfg.Timeout, Transport: tr},
|
||||
stream: &http.Client{Transport: tr},
|
||||
sem: make(chan struct{}, cfg.MaxConcurrent),
|
||||
states: map[string]*ModelState{},
|
||||
}
|
||||
@ -632,7 +657,7 @@ func (p *Provider) ChatStream(ctx context.Context, req *types.ChatRequest) (<-ch
|
||||
}
|
||||
rc := make(chan respOrErr, 1)
|
||||
go func() {
|
||||
resp, err := p.doRaw(ctx, p.URL(), body, hdrs)
|
||||
resp, err := p.doRawStream(ctx, p.URL(), body, hdrs)
|
||||
rc <- respOrErr{resp, err}
|
||||
}()
|
||||
|
||||
@ -770,6 +795,18 @@ func (p *Provider) doRaw(ctx context.Context, url, body string, hdr http.Header)
|
||||
return p.client.Do(httpReq)
|
||||
}
|
||||
|
||||
// doRawStream is the streaming variant of doRaw: it uses the no-overall-timeout
|
||||
// stream client so a long SSE body is not cut by client.Timeout. The transport
|
||||
// still enforces ResponseHeaderTimeout on time-to-first-byte.
|
||||
func (p *Provider) doRawStream(ctx context.Context, url, body string, hdr http.Header) (*http.Response, error) {
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader([]byte(body)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header = hdr
|
||||
return p.stream.Do(httpReq)
|
||||
}
|
||||
|
||||
func marshalTransform(vm *lua.VM, adapter, fn string, v interface{}) (string, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user