| package amp |
|
|
| import ( |
| "context" |
| "errors" |
| "net" |
| "net/http" |
| "net/http/httputil" |
| "strings" |
|
|
| "github.com/gin-gonic/gin" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" |
| "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" |
| "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/claude" |
| "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/gemini" |
| "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers/openai" |
| log "github.com/sirupsen/logrus" |
| ) |
|
|
| |
| |
| type clientAPIKeyContextKey struct{} |
|
|
| |
| |
| func clientAPIKeyMiddleware() gin.HandlerFunc { |
| return func(c *gin.Context) { |
| |
| if apiKey, exists := c.Get("apiKey"); exists { |
| if keyStr, ok := apiKey.(string); ok && keyStr != "" { |
| |
| ctx := context.WithValue(c.Request.Context(), clientAPIKeyContextKey{}, keyStr) |
| c.Request = c.Request.WithContext(ctx) |
| } |
| } |
| c.Next() |
| } |
| } |
|
|
| |
| |
| func getClientAPIKeyFromContext(ctx context.Context) string { |
| if val := ctx.Value(clientAPIKeyContextKey{}); val != nil { |
| if keyStr, ok := val.(string); ok { |
| return keyStr |
| } |
| } |
| return "" |
| } |
|
|
| |
| |
| func (m *AmpModule) localhostOnlyMiddleware() gin.HandlerFunc { |
| return func(c *gin.Context) { |
| |
| if !m.IsRestrictedToLocalhost() { |
| c.Next() |
| return |
| } |
|
|
| |
| |
| remoteAddr := c.Request.RemoteAddr |
|
|
| |
| host, _, err := net.SplitHostPort(remoteAddr) |
| if err != nil { |
| |
| host = remoteAddr |
| } |
|
|
| |
| ip := net.ParseIP(host) |
| if ip == nil { |
| log.Warnf("amp management: invalid RemoteAddr %s, denying access", remoteAddr) |
| c.AbortWithStatusJSON(403, gin.H{ |
| "error": "Access denied: management routes restricted to localhost", |
| }) |
| return |
| } |
|
|
| |
| if !ip.IsLoopback() { |
| log.Warnf("amp management: non-localhost connection from %s attempted access, denying", remoteAddr) |
| c.AbortWithStatusJSON(403, gin.H{ |
| "error": "Access denied: management routes restricted to localhost", |
| }) |
| return |
| } |
|
|
| c.Next() |
| } |
| } |
|
|
| |
| |
| func noCORSMiddleware() gin.HandlerFunc { |
| return func(c *gin.Context) { |
| |
| c.Header("Access-Control-Allow-Origin", "") |
| c.Header("Access-Control-Allow-Methods", "") |
| c.Header("Access-Control-Allow-Headers", "") |
| c.Header("Access-Control-Allow-Credentials", "") |
|
|
| |
| if c.Request.Method == "OPTIONS" { |
| c.AbortWithStatus(403) |
| return |
| } |
|
|
| c.Next() |
| } |
| } |
|
|
| |
| |
| func (m *AmpModule) managementAvailabilityMiddleware() gin.HandlerFunc { |
| return func(c *gin.Context) { |
| if m.getProxy() == nil { |
| logging.SkipGinRequestLogging(c) |
| c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{ |
| "error": "amp upstream proxy not available", |
| }) |
| return |
| } |
| c.Next() |
| } |
| } |
|
|
| |
| func wrapManagementAuth(auth gin.HandlerFunc, prefixes ...string) gin.HandlerFunc { |
| return func(c *gin.Context) { |
| path := c.Request.URL.Path |
| for _, prefix := range prefixes { |
| if strings.HasPrefix(path, prefix) && (len(path) == len(prefix) || path[len(prefix)] == '/') { |
| c.Next() |
| return |
| } |
| } |
| auth(c) |
| } |
| } |
|
|
| |
| |
| |
| |
| func (m *AmpModule) registerManagementRoutes(engine *gin.Engine, baseHandler *handlers.BaseAPIHandler, auth gin.HandlerFunc) { |
| ampAPI := engine.Group("/api") |
|
|
| |
| ampAPI.Use(m.managementAvailabilityMiddleware(), noCORSMiddleware()) |
|
|
| |
| ampAPI.Use(m.localhostOnlyMiddleware()) |
|
|
| |
| var authWithBypass gin.HandlerFunc |
| if auth != nil { |
| ampAPI.Use(auth) |
| authWithBypass = wrapManagementAuth(auth, "/threads", "/auth", "/docs", "/settings") |
| } |
|
|
| |
| ampAPI.Use(clientAPIKeyMiddleware()) |
|
|
| |
| proxyHandler := func(c *gin.Context) { |
| |
| defer func() { |
| if rec := recover(); rec != nil { |
| if err, ok := rec.(error); ok && errors.Is(err, http.ErrAbortHandler) { |
| |
| return |
| } |
| panic(rec) |
| } |
| }() |
|
|
| proxy := m.getProxy() |
| if proxy == nil { |
| c.JSON(503, gin.H{"error": "amp upstream proxy not available"}) |
| return |
| } |
| proxy.ServeHTTP(c.Writer, c.Request) |
| } |
|
|
| |
| ampAPI.Any("/internal", proxyHandler) |
| ampAPI.Any("/internal/*path", proxyHandler) |
| ampAPI.Any("/user", proxyHandler) |
| ampAPI.Any("/user/*path", proxyHandler) |
| ampAPI.Any("/auth", proxyHandler) |
| ampAPI.Any("/auth/*path", proxyHandler) |
| ampAPI.Any("/meta", proxyHandler) |
| ampAPI.Any("/meta/*path", proxyHandler) |
| ampAPI.Any("/ads", proxyHandler) |
| ampAPI.Any("/telemetry", proxyHandler) |
| ampAPI.Any("/telemetry/*path", proxyHandler) |
| ampAPI.Any("/threads", proxyHandler) |
| ampAPI.Any("/threads/*path", proxyHandler) |
| ampAPI.Any("/otel", proxyHandler) |
| ampAPI.Any("/otel/*path", proxyHandler) |
| ampAPI.Any("/tab", proxyHandler) |
| ampAPI.Any("/tab/*path", proxyHandler) |
|
|
| |
| |
| rootMiddleware := []gin.HandlerFunc{m.managementAvailabilityMiddleware(), noCORSMiddleware(), m.localhostOnlyMiddleware()} |
| if authWithBypass != nil { |
| rootMiddleware = append(rootMiddleware, authWithBypass) |
| } |
| |
| rootMiddleware = append(rootMiddleware, clientAPIKeyMiddleware()) |
| engine.GET("/threads", append(rootMiddleware, proxyHandler)...) |
| engine.GET("/threads/*path", append(rootMiddleware, proxyHandler)...) |
| engine.GET("/docs", append(rootMiddleware, proxyHandler)...) |
| engine.GET("/docs/*path", append(rootMiddleware, proxyHandler)...) |
| engine.GET("/settings", append(rootMiddleware, proxyHandler)...) |
| engine.GET("/settings/*path", append(rootMiddleware, proxyHandler)...) |
|
|
| engine.GET("/threads.rss", append(rootMiddleware, proxyHandler)...) |
| engine.GET("/news.rss", append(rootMiddleware, proxyHandler)...) |
|
|
| |
| |
| |
| engine.Any("/auth", append(rootMiddleware, proxyHandler)...) |
| engine.Any("/auth/*path", append(rootMiddleware, proxyHandler)...) |
|
|
| |
| |
| |
| |
| geminiHandlers := gemini.NewGeminiAPIHandler(baseHandler) |
| geminiBridge := createGeminiBridgeHandler(geminiHandlers.GeminiHandler) |
| geminiV1Beta1Fallback := NewFallbackHandlerWithMapper(func() *httputil.ReverseProxy { |
| return m.getProxy() |
| }, m.modelMapper, m.forceModelMappings) |
| geminiV1Beta1Handler := geminiV1Beta1Fallback.WrapHandler(geminiBridge) |
|
|
| |
| |
| |
| ampAPI.Any("/provider/google/v1beta1/*path", func(c *gin.Context) { |
| if c.Request.Method == "POST" { |
| if path := c.Param("path"); strings.Contains(path, "/models/") { |
| |
| |
| geminiV1Beta1Handler(c) |
| return |
| } |
| } |
| |
| proxyHandler(c) |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func (m *AmpModule) registerProviderAliases(engine *gin.Engine, baseHandler *handlers.BaseAPIHandler, auth gin.HandlerFunc) { |
| |
| openaiHandlers := openai.NewOpenAIAPIHandler(baseHandler) |
| geminiHandlers := gemini.NewGeminiAPIHandler(baseHandler) |
| claudeCodeHandlers := claude.NewClaudeCodeAPIHandler(baseHandler) |
| openaiResponsesHandlers := openai.NewOpenAIResponsesAPIHandler(baseHandler) |
|
|
| |
| |
| |
| fallbackHandler := NewFallbackHandlerWithMapper(func() *httputil.ReverseProxy { |
| return m.getProxy() |
| }, m.modelMapper, m.forceModelMappings) |
|
|
| |
| ampProviders := engine.Group("/api/provider") |
| if auth != nil { |
| ampProviders.Use(auth) |
| } |
| |
| ampProviders.Use(clientAPIKeyMiddleware()) |
|
|
| provider := ampProviders.Group("/:provider") |
|
|
| |
| ampModelsHandler := func(c *gin.Context) { |
| providerName := strings.ToLower(c.Param("provider")) |
|
|
| switch providerName { |
| case "anthropic": |
| claudeCodeHandlers.ClaudeModels(c) |
| case "google": |
| geminiHandlers.GeminiModels(c) |
| default: |
| |
| openaiHandlers.OpenAIModels(c) |
| } |
| } |
|
|
| |
| |
| provider.GET("/models", ampModelsHandler) |
| provider.POST("/chat/completions", fallbackHandler.WrapHandler(openaiHandlers.ChatCompletions)) |
| provider.POST("/completions", fallbackHandler.WrapHandler(openaiHandlers.Completions)) |
| provider.POST("/responses", fallbackHandler.WrapHandler(openaiResponsesHandlers.Responses)) |
|
|
| |
| v1Amp := provider.Group("/v1") |
| { |
| v1Amp.GET("/models", ampModelsHandler) |
|
|
| |
| v1Amp.POST("/chat/completions", fallbackHandler.WrapHandler(openaiHandlers.ChatCompletions)) |
| v1Amp.POST("/completions", fallbackHandler.WrapHandler(openaiHandlers.Completions)) |
| v1Amp.POST("/responses", fallbackHandler.WrapHandler(openaiResponsesHandlers.Responses)) |
|
|
| |
| v1Amp.POST("/messages", fallbackHandler.WrapHandler(claudeCodeHandlers.ClaudeMessages)) |
| v1Amp.POST("/messages/count_tokens", fallbackHandler.WrapHandler(claudeCodeHandlers.ClaudeCountTokens)) |
| } |
|
|
| |
| |
| v1betaAmp := provider.Group("/v1beta") |
| { |
| v1betaAmp.GET("/models", geminiHandlers.GeminiModels) |
| v1betaAmp.POST("/models/*action", fallbackHandler.WrapHandler(geminiHandlers.GeminiHandler)) |
| v1betaAmp.GET("/models/*action", geminiHandlers.GeminiGetHandler) |
| } |
| } |
|
|