139 lines
3.9 KiB
Go
139 lines
3.9 KiB
Go
package router
|
|
|
|
import (
|
|
"cursor-api-proxy/internal/config"
|
|
"cursor-api-proxy/internal/handlers"
|
|
"cursor-api-proxy/internal/httputil"
|
|
"cursor-api-proxy/internal/logger"
|
|
"cursor-api-proxy/internal/pool"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
type RouterOptions struct {
|
|
Version string
|
|
Config config.BridgeConfig
|
|
ModelCache *handlers.ModelCacheRef
|
|
LastModel *string
|
|
Pool pool.PoolHandle
|
|
}
|
|
|
|
func NewRouter(opts RouterOptions) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
cfg := opts.Config
|
|
pathname := r.URL.Path
|
|
method := r.Method
|
|
remoteAddress := r.RemoteAddr
|
|
if r.Header.Get("X-Real-IP") != "" {
|
|
remoteAddress = r.Header.Get("X-Real-IP")
|
|
}
|
|
|
|
logger.LogIncoming(method, pathname, remoteAddress)
|
|
|
|
defer func() {
|
|
logger.AppendSessionLine(cfg.SessionsLogPath, method, pathname, remoteAddress, 200)
|
|
}()
|
|
|
|
if cfg.RequiredKey != "" {
|
|
token := httputil.ExtractBearerToken(r)
|
|
if token != cfg.RequiredKey {
|
|
httputil.WriteJSON(w, 401, map[string]interface{}{
|
|
"error": map[string]string{"message": "Invalid API key", "code": "unauthorized"},
|
|
}, nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case method == "GET" && pathname == "/health":
|
|
handlers.HandleHealth(w, r, opts.Version, cfg)
|
|
|
|
case method == "GET" && pathname == "/v1/models":
|
|
opts.ModelCache.HandleModels(w, r, cfg)
|
|
|
|
case method == "POST" && pathname == "/v1/chat/completions":
|
|
raw, err := httputil.ReadBody(r)
|
|
if err != nil {
|
|
httputil.WriteJSON(w, 400, map[string]interface{}{
|
|
"error": map[string]string{"message": "failed to read body", "code": "bad_request"},
|
|
}, nil)
|
|
return
|
|
}
|
|
handlers.HandleChatCompletions(w, r, cfg, opts.Pool, opts.LastModel, raw, method, pathname, remoteAddress)
|
|
|
|
case method == "POST" && pathname == "/v1/messages":
|
|
raw, err := httputil.ReadBody(r)
|
|
if err != nil {
|
|
httputil.WriteJSON(w, 400, map[string]interface{}{
|
|
"error": map[string]string{"message": "failed to read body", "code": "bad_request"},
|
|
}, nil)
|
|
return
|
|
}
|
|
handlers.HandleAnthropicMessages(w, r, cfg, opts.Pool, opts.LastModel, raw, method, pathname, remoteAddress)
|
|
|
|
case (method == "POST" || method == "GET") && pathname == "/v1/completions":
|
|
httputil.WriteJSON(w, 404, map[string]interface{}{
|
|
"error": map[string]string{
|
|
"message": "Legacy completions endpoint is not supported. Use POST /v1/chat/completions instead.",
|
|
"code": "not_found",
|
|
},
|
|
}, nil)
|
|
|
|
case pathname == "/v1/embeddings":
|
|
httputil.WriteJSON(w, 404, map[string]interface{}{
|
|
"error": map[string]string{
|
|
"message": "Embeddings are not supported by this proxy.",
|
|
"code": "not_found",
|
|
},
|
|
}, nil)
|
|
|
|
default:
|
|
httputil.WriteJSON(w, 404, map[string]interface{}{
|
|
"error": map[string]string{"message": "Not found", "code": "not_found"},
|
|
}, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func recoveryMiddleware(logPath string, next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
defer func() {
|
|
if rec := recover(); rec != nil {
|
|
msg := fmt.Sprintf("%v", rec)
|
|
fmt.Fprintf(os.Stderr, "[%s] Proxy panic: %s\n", time.Now().UTC().Format(time.RFC3339), msg)
|
|
line := fmt.Sprintf("%s ERROR %s %s %s %s\n",
|
|
time.Now().UTC().Format(time.RFC3339), r.Method, r.URL.Path, r.RemoteAddr,
|
|
msg[:min(200, len(msg))])
|
|
if f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
|
|
_, _ = f.WriteString(line)
|
|
f.Close()
|
|
}
|
|
if !isHeaderWritten(w) {
|
|
httputil.WriteJSON(w, 500, map[string]interface{}{
|
|
"error": map[string]string{"message": msg, "code": "internal_error"},
|
|
}, nil)
|
|
}
|
|
}
|
|
}()
|
|
next(w, r)
|
|
}
|
|
}
|
|
|
|
func isHeaderWritten(w http.ResponseWriter) bool {
|
|
// Can't reliably detect without wrapping; always try to write
|
|
return false
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func WrapWithRecovery(logPath string, handler http.HandlerFunc) http.HandlerFunc {
|
|
return recoveryMiddleware(logPath, handler)
|
|
}
|