feat(stats): gateway request stats/audit API + polish scratch sort animations (link chain, reset, cleanup)

This commit is contained in:
root
2026-08-08 13:34:40 +08:00
parent 31eefe9288
commit dec03238dd
5 changed files with 646 additions and 131 deletions

View File

@ -147,6 +147,8 @@ func (g *Gateway) handleChat(w http.ResponseWriter, r *http.Request) {
effective = firstModel(cands[0])
}
ctx := r.Context()
done := g.stats.Begin()
defer done()
inner := &types.ChatRequest{
Model: effective,
@ -159,11 +161,19 @@ func (g *Gateway) handleChat(w http.ResponseWriter, r *http.Request) {
DisableThinking: req.DisableThinking,
ExtraBody: req.ExtraBody,
}
rec := &Req{
Key: keyID(reqKey(ctx)),
Type: "chat",
Model: effective,
Source: firstSource(cands),
OK: false,
}
if req.Stream {
g.streamChat(w, ctx, cands, inner, effective)
rec.Type = "stream"
g.streamChat(w, ctx, cands, inner, effective, rec)
return
}
g.singleChat(w, ctx, cands, inner, effective)
g.singleChat(w, ctx, cands, inner, effective, rec)
}
func normalizeModel(m string) string {
@ -220,12 +230,32 @@ func toolCallsWire(tcs []types.ToolCall) json.RawMessage {
return b
}
func (g *Gateway) singleChat(w http.ResponseWriter, ctx context.Context, cands []*provider.Provider, req *types.ChatRequest, effective string) {
func firstSource(cands []*provider.Provider) string {
if len(cands) > 0 {
return cands[0].Name()
}
return ""
}
func (g *Gateway) singleChat(w http.ResponseWriter, ctx context.Context, cands []*provider.Provider, req *types.ChatRequest, effective string, rec *Req) {
rec.LatMs = 0
t0 := time.Now()
resp, err := g.core.Scheduler().Chat(ctx, scheduler.FromRegistry(cands), req)
rec.LatMs = time.Since(t0).Milliseconds()
if err != nil {
rec.OK = false
rec.Status = http.StatusBadGateway
rec.Err = err.Error()
g.writeRec(rec)
writeError(w, http.StatusBadGateway, "upstream_error", err.Error())
return
}
rec.OK = true
rec.Status = http.StatusOK
rec.Prompt = int64(resp.TokenUsage.Prompt)
rec.Compl = int64(resp.TokenUsage.Completion)
rec.Source = firstSource(cands)
g.writeRec(rec)
msg := RespMessage{Role: "assistant", Content: resp.Content}
if resp.ReasoningContent != "" {
msg.ReasoningContent = resp.ReasoningContent
@ -246,9 +276,31 @@ func (g *Gateway) singleChat(w http.ResponseWriter, ctx context.Context, cands [
writeJSON(w, http.StatusOK, out)
}
func (g *Gateway) streamChat(w http.ResponseWriter, ctx context.Context, cands []*provider.Provider, req *types.ChatRequest, effective string) {
// writeRec records a finished request (audit + aggregates).
func (g *Gateway) writeRec(rec *Req) {
if rec == nil {
return
}
if rec.Time == 0 {
rec.Time = time.Now().UnixMilli()
}
g.stats.Record(*rec)
}
func (g *Gateway) streamChat(w http.ResponseWriter, ctx context.Context, cands []*provider.Provider, req *types.ChatRequest, effective string, rec *Req) {
rec.LatMs = 0
t0 := time.Now()
rec.OK = true
rec.Status = http.StatusOK
defer func() {
rec.LatMs = time.Since(t0).Milliseconds()
g.writeRec(rec)
}()
chunks, err := g.core.Scheduler().ChatStream(ctx, scheduler.FromRegistry(cands), req)
if err != nil {
rec.OK = false
rec.Status = http.StatusBadGateway
rec.Err = err.Error()
writeError(w, http.StatusBadGateway, "upstream_error", err.Error())
return
}
@ -297,6 +349,7 @@ func (g *Gateway) streamChat(w http.ResponseWriter, ctx context.Context, cands [
choice.FinishReason = &stop
}
chunk.Choices = []ChunkChoice{choice}
rec.Compl += int64(len(ck.Content)+len(ck.ReasoningContent)) / 3
if !send(chunk) {
return
}
@ -336,11 +389,23 @@ func (g *Gateway) handleImage(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusServiceUnavailable, "no_provider", "no image source configured")
return
}
done := g.stats.Begin()
defer done()
rec := &Req{Key: keyID(reqKey(r.Context())), Type: "image", Model: model, Source: firstSource(cands), OK: false}
t0 := time.Now()
resp, err := g.core.Scheduler().Image(r.Context(), scheduler.FromRegistry(cands), &req)
rec.LatMs = time.Since(t0).Milliseconds()
if err != nil {
rec.Status = http.StatusBadGateway
rec.Err = err.Error()
g.writeRec(rec)
writeError(w, http.StatusBadGateway, "upstream_error", err.Error())
return
}
rec.OK = true
rec.Status = http.StatusOK
rec.Compl = int64(len(resp.ImageData))
g.writeRec(rec)
writeJSON(w, http.StatusOK, types.ImageGenResponse{
Created: time.Now().Unix(),
Data: resp.ImageData,