Files
2026-10-23 18:35:44 +08:00

274 lines
7.8 KiB
Go

package server
import (
"ai-router/internal/protocol"
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"sort"
"strings"
"time"
)
func resolve(cs []channel, k apiKey, model string) (channel, string, error) {
name, alias, ok := strings.Cut(model, "/")
if !ok || alias == "" {
return channel{}, "", errors.New("model must be channel/alias")
}
for _, c := range cs {
if c.Name != name {
continue
}
if !c.Enabled || !k.allows(c.ID) {
return channel{}, "", errors.New("channel unavailable")
}
if actual, ok := c.Models[alias]; ok {
return c, actual, nil
}
return channel{}, "", errors.New("model unavailable")
}
return channel{}, "", errors.New("channel unavailable")
}
func (s *Server) proxy(w http.ResponseWriter, r *http.Request) {
kinds := map[string]protocol.Kind{"/v1/chat/completions": protocol.Chat, "/v1/responses": protocol.Responses, "/v1/messages": protocol.Anthropic}
down, ok := kinds[r.URL.Path]
models := r.URL.Path == "/v1/models"
if !ok && !models {
errJSON(w, 404, "not found")
return
}
if (models && r.Method != "GET") || (!models && r.Method != "POST") {
errJSON(w, 405, "method not allowed")
return
}
ip := s.clientIP(r)
if !s.authAllowed(ip) {
w.Header().Set("Retry-After", "60")
errJSON(w, 429, "request limit exceeded")
return
}
k, e := s.authenticate(r)
if e != nil {
s.authFail(ip)
errJSON(w, 401, "invalid API key")
return
}
release, retry := s.limiter.acquire(k.ID, time.Now())
if release == nil {
w.Header().Set("Retry-After", retry)
errJSON(w, 429, "request limit exceeded")
return
}
start := time.Now()
l := requestLog{CreatedAt: start.UTC().Format(time.RFC3339Nano), KeyID: k.ID, KeyName: k.Name, Downstream: string(down), Status: 500}
// Release the concurrency slot before the log write, which can block on a
// full queue; rate-limited and unauthenticated requests stay unlogged.
defer func() {
release()
if !models {
l.DurationMS = time.Since(start).Milliseconds()
s.record(l)
}
}()
fail := func(status int, msg string) { l.Status = status; l.Error = msg; errJSON(w, status, msg) }
cs, e := s.loadChannels()
if e != nil {
fail(500, "database error")
return
}
if models {
ids := []string{}
for _, c := range cs {
if c.Enabled && k.allows(c.ID) {
for alias := range c.Models {
ids = append(ids, c.Name+"/"+alias)
}
}
}
sort.Strings(ids)
out := []any{}
for _, id := range ids {
out = append(out, map[string]string{"id": id, "object": "model", "owned_by": "ai-router"})
}
l.Status = 200
writeJSON(w, map[string]any{"object": "list", "data": out})
return
}
var b []byte
e = readBody(w, r, maxBody, bodyReadTimeout, func() error {
var err error
b, err = io.ReadAll(r.Body)
return err
})
if e != nil {
var tooBig *http.MaxBytesError
if errors.As(e, &tooBig) {
fail(413, "request body too large")
} else {
fail(400, "request body unreadable")
}
return
}
var req struct {
Model string `json:"model"`
Stream bool `json:"stream"`
}
if json.Unmarshal(b, &req) != nil || req.Model == "" {
fail(400, "invalid request")
return
}
l.RequestModel = req.Model
l.Stream = req.Stream
c, actual, e := resolve(cs, k, req.Model)
if e != nil {
fail(403, "model or channel unavailable")
return
}
l.Channel = c.Name
l.ActualModel = actual
l.Upstream = c.Protocol
up := protocol.Kind(c.Protocol)
payload, e := protocol.ConvertRequest(down, up, b, actual)
if e != nil {
fail(400, "unsupported request conversion")
return
}
if req.Stream && up == protocol.Chat {
var obj map[string]json.RawMessage
if json.Unmarshal(payload, &obj) == nil {
options := map[string]json.RawMessage{}
if raw, ok := obj["stream_options"]; ok {
_ = json.Unmarshal(raw, &options)
}
if options == nil {
options = map[string]json.RawMessage{}
}
options["include_usage"] = json.RawMessage("true")
obj["stream_options"], _ = json.Marshal(options)
payload, _ = json.Marshal(obj)
}
}
suffix := map[protocol.Kind]string{protocol.Chat: "/chat/completions", protocol.Responses: "/responses", protocol.Anthropic: "/v1/messages"}[up]
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Minute)
defer cancel()
request, e := http.NewRequestWithContext(ctx, "POST", c.BaseURL+suffix, bytes.NewReader(payload))
if e != nil {
fail(502, "invalid upstream configuration")
return
}
request.Header.Set("Content-Type", "application/json")
if up == protocol.Anthropic {
request.Header.Set("x-api-key", c.APIKey)
request.Header.Set("anthropic-version", "2023-06-01")
if down == up {
if v := r.Header.Get("anthropic-version"); safeForwardedHeader(v, 32) {
request.Header.Set("anthropic-version", v)
}
if v := r.Header.Get("anthropic-beta"); safeForwardedHeader(v, 256) {
request.Header.Set("anthropic-beta", v)
}
}
} else {
request.Header.Set("Authorization", "Bearer "+c.APIKey)
}
response, e := s.client.Do(request)
if e != nil {
fail(502, "upstream request failed")
return
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
status := response.StatusCode
if status < 400 {
status = 502
}
fail(status, "upstream rejected request")
return
}
if req.Stream {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no")
w.WriteHeader(response.StatusCode)
l.Status = response.StatusCode
streamWriter := &deadlineWriter{ResponseWriter: w}
u, e := protocol.Stream(ctx, streamWriter, response.Body, up, down, req.Model)
l.Usage = u
if e != nil {
l.Status = 502
l.Error = "upstream stream interrupted"
l.Usage.Known = false
if ctx.Err() == nil {
event := "data: {\"error\":{\"type\":\"upstream_error\",\"message\":\"upstream stream interrupted\"}}\n\n"
if down != protocol.Chat {
event = "event: error\n" + event
}
_, _ = io.WriteString(streamWriter, event)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
return
}
b, e = io.ReadAll(io.LimitReader(response.Body, maxBody+1))
if e != nil || len(b) > maxBody {
fail(502, "invalid upstream response")
return
}
l.Usage = protocol.ExtractUsage(up, b)
out, e := protocol.ConvertResponse(up, down, b)
if e != nil {
fail(502, "unsupported response conversion")
return
}
l.Status = response.StatusCode
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.StatusCode)
if _, e = w.Write(out); e != nil {
l.Status = 499
l.Error = "client disconnected"
}
}
type deadlineWriter struct{ http.ResponseWriter }
// safeForwardedHeader reports whether v is a short printable token list that
// is safe to forward upstream; anything else is dropped so clients cannot
// smuggle arbitrary features or exotic header content past the channel
// configuration.
func safeForwardedHeader(v string, max int) bool {
if len(v) == 0 || len(v) > max {
return false
}
for i := 0; i < len(v); i++ {
c := v[i]
switch {
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
case c == '-' || c == '_' || c == '.' || c == ',' || c == ' ':
default:
return false
}
}
return true
}
func (w *deadlineWriter) Unwrap() http.ResponseWriter { return w.ResponseWriter }
func (w *deadlineWriter) WriteHeader(status int) {
_ = http.NewResponseController(w.ResponseWriter).SetWriteDeadline(time.Now().Add(30 * time.Second))
w.ResponseWriter.WriteHeader(status)
}
func (w *deadlineWriter) Write(b []byte) (int, error) {
_ = http.NewResponseController(w.ResponseWriter).SetWriteDeadline(time.Now().Add(30 * time.Second))
return w.ResponseWriter.Write(b)
}
func (w *deadlineWriter) FlushError() error {
_ = http.NewResponseController(w.ResponseWriter).SetWriteDeadline(time.Now().Add(30 * time.Second))
return http.NewResponseController(w.ResponseWriter).Flush()
}
func (w *deadlineWriter) Flush() { _ = w.FlushError() }