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:
root
2026-08-11 15:21:05 +08:00
parent 854b3e2e37
commit 20d2268546
8 changed files with 125 additions and 19 deletions

View File

@ -75,6 +75,10 @@ type sourcePayload struct {
}
func (g *Gateway) handleSourcesAPI(w http.ResponseWriter, r *http.Request) {
if reqRole(r.Context()) != "admin" {
writeError(w, http.StatusForbidden, "forbidden", "admin role required")
return
}
path := strings.TrimPrefix(r.URL.Path, "/api/sources")
path = strings.Trim(path, "/")
@ -121,6 +125,7 @@ func (g *Gateway) handleSourcesAPI(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "")
}
}
// handleStatsAPI returns per-key / per-model / per-source usage aggregates and
// the recent request audit trail.
func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) {
@ -173,7 +178,7 @@ func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) {
cw.Flush()
return
}
if r.URL.Query().Get("export") == "keys-csv" {
if r.URL.Query().Get("export") == "keys-csv" {
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=llmsproxy-keys.csv")
cw := csv.NewWriter(w)