274 lines
7.8 KiB
Go
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() }
|