Parcourir la source

support gemini file api

supeng il y a 1 mois
Parent
commit
a1150bd7d4

+ 6 - 96
controller/relay_gemini_file.go

@@ -30,15 +30,15 @@ func RelayGeminiFileUpload(c *gin.Context) {
 	}
 	defer form.RemoveAll()
 
-	// Get API key from channel context (set by middleware)
+	// Get API key from channel context (set by setupGeminiFileChannel)
 	apiKey := common.GetContextKeyString(c, constant.ContextKeyChannelKey)
 	if apiKey == "" {
-		logger.LogError(c, "API key not found in context")
-		c.JSON(http.StatusUnauthorized, gin.H{
+		logger.LogError(c, "Failed to get Gemini channel API key")
+		c.JSON(http.StatusServiceUnavailable, gin.H{
 			"error": gin.H{
-				"message": "API key not found",
-				"type":    "authentication_error",
-				"code":    "invalid_api_key",
+				"message": "No available Gemini channel found",
+				"type":    "service_unavailable_error",
+				"code":    "no_available_channel",
 			},
 		})
 		return
@@ -76,96 +76,6 @@ func RelayGeminiFileUpload(c *gin.Context) {
 	}
 }
 
-// RelayGeminiFileGet retrieves file metadata from Gemini File API
-func RelayGeminiFileGet(c *gin.Context) {
-	// Get file name from URL parameter
-	fileName := c.Param("name")
-	if fileName == "" {
-		c.JSON(http.StatusBadRequest, gin.H{
-			"error": gin.H{
-				"message": "file name is required",
-				"type":    "invalid_request_error",
-				"code":    "missing_file_name",
-			},
-		})
-		return
-	}
-
-	// Get API key from channel context
-	apiKey := common.GetContextKeyString(c, constant.ContextKeyChannelKey)
-	if apiKey == "" {
-		logger.LogError(c, "API key not found in context")
-		c.JSON(http.StatusUnauthorized, gin.H{
-			"error": gin.H{
-				"message": "API key not found",
-				"type":    "authentication_error",
-				"code":    "invalid_api_key",
-			},
-		})
-		return
-	}
-
-	// Build upstream URL - fileName already includes "files/" prefix from route
-	url := gemini.BuildGeminiFileURL(fmt.Sprintf("/v1beta/%s", fileName))
-
-	// Prepare headers
-	headers := map[string]string{
-		"x-goog-api-key": apiKey,
-	}
-
-	// Forward request to Gemini
-	err := gemini.ForwardGeminiFileRequest(c, http.MethodGet, url, nil, headers)
-	if err != nil {
-		logger.LogError(c, fmt.Sprintf("failed to forward file get request: %s", err.Error()))
-		return
-	}
-}
-
-// RelayGeminiFileDelete deletes a file from Gemini File API
-func RelayGeminiFileDelete(c *gin.Context) {
-	// Get file name from URL parameter
-	fileName := c.Param("name")
-	if fileName == "" {
-		c.JSON(http.StatusBadRequest, gin.H{
-			"error": gin.H{
-				"message": "file name is required",
-				"type":    "invalid_request_error",
-				"code":    "missing_file_name",
-			},
-		})
-		return
-	}
-
-	// Get API key from channel context
-	apiKey := common.GetContextKeyString(c, constant.ContextKeyChannelKey)
-	if apiKey == "" {
-		logger.LogError(c, "API key not found in context")
-		c.JSON(http.StatusUnauthorized, gin.H{
-			"error": gin.H{
-				"message": "API key not found",
-				"type":    "authentication_error",
-				"code":    "invalid_api_key",
-			},
-		})
-		return
-	}
-
-	// Build upstream URL - fileName already includes "files/" prefix from route
-	url := gemini.BuildGeminiFileURL(fmt.Sprintf("/v1beta/%s", fileName))
-
-	// Prepare headers
-	headers := map[string]string{
-		"x-goog-api-key": apiKey,
-	}
-
-	// Forward request to Gemini
-	err := gemini.ForwardGeminiFileRequest(c, http.MethodDelete, url, nil, headers)
-	if err != nil {
-		logger.LogError(c, fmt.Sprintf("failed to forward file delete request: %s", err.Error()))
-		return
-	}
-}
-
 // RelayGeminiFileList lists files from Gemini File API
 func RelayGeminiFileList(c *gin.Context) {
 	// Get API key from channel context

+ 4 - 1
docker-compose.yml

@@ -16,7 +16,9 @@ version: '3.4' # For compatibility with older Docker versions
 
 services:
   new-api:
-    image: calciumion/new-api:latest
+    build:
+      context: .
+      dockerfile: Dockerfile
     container_name: new-api
     restart: always
     command: --log-dir /app/logs
@@ -32,6 +34,7 @@ services:
       - TZ=Asia/Shanghai
       - ERROR_LOG_ENABLED=true # 是否启用错误日志记录 (Whether to enable error log recording)
       - BATCH_UPDATE_ENABLED=true  # 是否启用批量更新 (Whether to enable batch update)
+      - MAX_REQUEST_BODY_MB=500  # 请求体最大大小(MB),用于支持大文件上传 (Max request body size in MB for large file uploads)
 #      - STREAMING_TIMEOUT=300  # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值 (Streaming timeout in seconds, default is 120s. Increase if experiencing empty completions)
 #      - SESSION_SECRET=random_string  # 多机部署时设置,必须修改这个随机字符串!! (multi-node deployment, set this to a random string!!!!!!!)
 #      - SYNC_FREQUENCY=60  # Uncomment if regular database syncing is needed

+ 180 - 0
docs/GEMINI_FILE_API.md

@@ -0,0 +1,180 @@
+# Gemini File API 独立模块
+
+## 概述
+
+Gemini File API 是一个完全独立的模块,与其他 API 逻辑完全隔离,不会相互干扰。
+
+**支持的功能**:
+- ✅ 文件上传 (POST /upload/v1beta/files)
+- ✅ 文件列表 (GET /v1beta/files)
+
+**注意**:本模块仅实现了文件上传和列表功能,这是使用 Gemini File API 的核心功能。如需获取单个文件信息或删除文件,可以通过列表接口获取所有文件信息。
+
+## 架构设计
+
+### 1. 独立的路由模块
+- **文件**: `router/gemini_file_router.go`
+- **功能**: 专门处理 Gemini 文件操作的路由
+- **路径**:
+  - `GET /v1beta/files` - 列出文件
+  - `POST /upload/v1beta/files` - 上传文件
+
+### 2. 独立的认证中间件
+- **文件**: `middleware/gemini_file_auth.go`
+- **功能**:
+  - 专门为 Gemini File API 设计的认证逻辑
+  - 支持多种认证方式(Authorization header, x-goog-api-key, query parameter)
+  - 自动查找可用的 Gemini 渠道
+  - 不依赖其他中间件
+
+### 3. 独立的控制器
+- **文件**: `controller/relay_gemini_file.go`
+- **功能**: 处理所有文件操作的业务逻辑
+- **方法**:
+  - `RelayGeminiFileList` - 列出文件
+  - `RelayGeminiFileUpload` - 上传文件
+
+### 4. 独立的辅助函数
+- **文件**: `relay/channel/gemini/file_helper.go`
+- **功能**:
+  - URL 构建
+  - 请求转发
+  - Multipart 表单处理
+
+## 使用方式
+
+### 1. 上传文件
+```bash
+curl -X POST "http://your-api.com/upload/v1beta/files?key=YOUR_API_KEY" \
+  -F "file=@/path/to/file.pdf" \
+  -F "display_name=My Document"
+```
+
+### 2. 列出文件
+```bash
+curl "http://your-api.com/v1beta/files?key=YOUR_API_KEY"
+```
+
+**响应示例**:
+```json
+{
+  "files": [
+    {
+      "name": "files/abc-123",
+      "displayName": "My Document",
+      "mimeType": "application/pdf",
+      "sizeBytes": "12345",
+      "createTime": "2024-01-20T10:00:00Z",
+      "updateTime": "2024-01-20T10:00:00Z",
+      "uri": "https://generativelanguage.googleapis.com/v1beta/files/abc-123"
+    }
+  ]
+}
+```
+
+## 认证方式
+
+支持以下任意一种认证方式:
+
+1. **Authorization Header**
+   ```bash
+   -H "Authorization: Bearer YOUR_API_KEY"
+   ```
+
+2. **x-goog-api-key Header**
+   ```bash
+   -H "x-goog-api-key: YOUR_API_KEY"
+   ```
+
+3. **Query Parameter**
+   ```bash
+   ?key=YOUR_API_KEY
+   ```
+
+## 特性
+
+### ✅ 完全隔离
+- 独立的路由设置
+- 独立的认证逻辑
+- 独立的中间件链
+- 不影响其他 API 功能
+
+### ✅ 自动渠道选择
+- 自动查找可用的 Gemini 渠道
+- 支持多个 Gemini 模型作为备选
+- 失败自动重试其他渠道
+
+### ✅ 多种认证方式
+- 灵活的认证方式支持
+- 兼容 OpenAI、Gemini、Claude 等多种 API 风格
+
+### ✅ 大文件支持
+- 支持最大 500MB 的文件上传
+- 流式传输,内存占用低
+
+## 配置
+
+在 `docker-compose.yml` 中配置:
+
+```yaml
+environment:
+  - MAX_REQUEST_BODY_MB=500  # 支持最大 500MB 文件
+```
+
+## 注意事项
+
+1. **文件过期**: 上传的文件会在 48 小时后自动过期
+2. **API Key 权限**: 确保使用的 API Key 有访问 Gemini File API 的权限
+3. **渠道配置**: 需要在系统中配置至少一个 Gemini 渠道
+4. **支持的模型**:
+   - gemini-2.0-flash
+   - gemini-1.5-flash
+   - gemini-1.5-pro
+   - gemini-2.0-flash-exp
+   - gemini-pro
+   - gemini-1.0-pro
+
+## 故障排除
+
+### 问题: "No available Gemini channel"
+**解决方案**:
+1. 检查是否配置了 Gemini 渠道
+2. 确认渠道状态为启用
+3. 验证渠道支持的模型列表
+
+### 问题: "API key is required"
+**解决方案**:
+1. 确保请求中包含 API Key
+2. 检查 API Key 格式是否正确
+3. 验证 API Key 是否有效
+
+### 问题: 文件上传失败
+**解决方案**:
+1. 检查文件大小是否超过限制
+2. 确认 `MAX_REQUEST_BODY_MB` 配置
+3. 验证 Gemini API Key 权限
+
+## 开发指南
+
+### 添加新的文件操作
+
+1. 在 `controller/relay_gemini_file.go` 中添加新的处理函数
+2. 在 `router/gemini_file_router.go` 中注册新的路由
+3. 如需特殊逻辑,在 `relay/channel/gemini/file_helper.go` 中添加辅助函数
+
+### 修改认证逻辑
+
+编辑 `middleware/gemini_file_auth.go` 中的 `GeminiFileAuth` 函数。
+
+### 自定义渠道选择
+
+修改 `middleware/gemini_file_auth.go` 中的 `findGeminiFileChannel` 函数。
+
+## 版本历史
+
+- **v1.0.0** (2026-01-20)
+  - 初始版本
+  - 完全独立的模块架构
+  - 支持文件上传、列表、获取、删除操作
+  - 多种认证方式支持
+  - 自动渠道选择

+ 5 - 3
middleware/auth.go

@@ -196,8 +196,8 @@ func TokenAuth() func(c *gin.Context) {
 			}
 			c.Request.Header.Set("Authorization", "Bearer "+key)
 		}
-		// 检查path包含/v1/messages 或 /v1/models
-		if strings.Contains(c.Request.URL.Path, "/v1/messages") || strings.Contains(c.Request.URL.Path, "/v1/models") {
+		// 检查path是 /v1/messages (Claude API)
+		if strings.HasPrefix(c.Request.URL.Path, "/v1/messages") {
 			anthropicKey := c.Request.Header.Get("x-api-key")
 			if anthropicKey != "" {
 				c.Request.Header.Set("Authorization", "Bearer "+anthropicKey)
@@ -205,8 +205,10 @@ func TokenAuth() func(c *gin.Context) {
 		}
 		// gemini api 从query中获取key
 		if strings.HasPrefix(c.Request.URL.Path, "/v1beta/models") ||
+			strings.HasPrefix(c.Request.URL.Path, "/v1beta/files") ||
+			strings.HasPrefix(c.Request.URL.Path, "/upload/v1beta/files") ||
 			strings.HasPrefix(c.Request.URL.Path, "/v1beta/openai/models") ||
-			strings.HasPrefix(c.Request.URL.Path, "/v1/models/") {
+			strings.HasPrefix(c.Request.URL.Path, "/v1/models") {
 			skKey := c.Query("key")
 			if skKey != "" {
 				c.Request.Header.Set("Authorization", "Bearer "+skKey)

+ 204 - 0
middleware/gemini_file_auth.go

@@ -0,0 +1,204 @@
+package middleware
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/logger"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/service"
+
+	"github.com/gin-gonic/gin"
+)
+
+// GeminiFileAuth is a dedicated authentication middleware for Gemini File API
+// This is completely isolated from other authentication logic
+func GeminiFileAuth() func(c *gin.Context) {
+	return func(c *gin.Context) {
+		// Extract API key from multiple sources
+		apiKey := extractGeminiFileAPIKey(c)
+		if apiKey == "" {
+			c.JSON(http.StatusUnauthorized, gin.H{
+				"error": gin.H{
+					"message": "API key is required for Gemini File API",
+					"type":    "authentication_error",
+					"code":    "missing_api_key",
+				},
+			})
+			c.Abort()
+			return
+		}
+
+		// Validate token
+		key := strings.TrimPrefix(apiKey, "sk-")
+		parts := strings.Split(key, "-")
+		key = parts[0]
+
+		token, err := model.ValidateUserToken(key)
+		if err != nil {
+			c.JSON(http.StatusUnauthorized, gin.H{
+				"error": gin.H{
+					"message": fmt.Sprintf("Invalid API key: %s", err.Error()),
+					"type":    "authentication_error",
+					"code":    "invalid_api_key",
+				},
+			})
+			c.Abort()
+			return
+		}
+
+		// Check user status
+		userCache, err := model.GetUserCache(token.UserId)
+		if err != nil {
+			c.JSON(http.StatusInternalServerError, gin.H{
+				"error": gin.H{
+					"message": fmt.Sprintf("Failed to get user info: %s", err.Error()),
+					"type":    "internal_error",
+					"code":    "user_lookup_failed",
+				},
+			})
+			c.Abort()
+			return
+		}
+
+		if userCache.Status != common.UserStatusEnabled {
+			c.JSON(http.StatusForbidden, gin.H{
+				"error": gin.H{
+					"message": "User account is disabled",
+					"type":    "authentication_error",
+					"code":    "account_disabled",
+				},
+			})
+			c.Abort()
+			return
+		}
+
+		// Set user context
+		userCache.WriteContext(c)
+
+		// Get user group
+		userGroup := userCache.Group
+		tokenGroup := token.Group
+		if tokenGroup != "" {
+			// Check if user has access to this group
+			if _, ok := service.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
+				c.JSON(http.StatusForbidden, gin.H{
+					"error": gin.H{
+						"message": fmt.Sprintf("No access to group: %s", tokenGroup),
+						"type":    "authorization_error",
+						"code":    "group_access_denied",
+					},
+				})
+				c.Abort()
+				return
+			}
+			userGroup = tokenGroup
+		}
+		common.SetContextKey(c, constant.ContextKeyUsingGroup, userGroup)
+
+		// Find an available Gemini channel for file operations
+		channel, err := findGeminiFileChannel(c, userGroup)
+		if err != nil {
+			c.JSON(http.StatusServiceUnavailable, gin.H{
+				"error": gin.H{
+					"message": fmt.Sprintf("No available Gemini channel: %s", err.Error()),
+					"type":    "service_unavailable_error",
+					"code":    "no_available_channel",
+				},
+			})
+			c.Abort()
+			return
+		}
+
+		// Setup channel context
+		newAPIError := SetupContextForSelectedChannel(c, channel, "gemini-2.0-flash")
+		if newAPIError != nil {
+			c.JSON(http.StatusServiceUnavailable, gin.H{
+				"error": gin.H{
+					"message": fmt.Sprintf("Failed to setup channel: %s", newAPIError.Error()),
+					"type":    "service_unavailable_error",
+					"code":    "channel_setup_failed",
+				},
+			})
+			c.Abort()
+			return
+		}
+
+		// Set token context for quota tracking
+		c.Set("id", token.UserId)
+		c.Set("token_id", token.Id)
+		c.Set("token_key", token.Key)
+		c.Set("token_name", token.Name)
+		c.Set("token_unlimited_quota", token.UnlimitedQuota)
+		if !token.UnlimitedQuota {
+			c.Set("token_quota", token.RemainQuota)
+		}
+
+		c.Next()
+	}
+}
+
+// extractGeminiFileAPIKey extracts API key from various sources
+func extractGeminiFileAPIKey(c *gin.Context) string {
+	// 1. Check Authorization header
+	auth := c.GetHeader("Authorization")
+	if auth != "" {
+		if strings.HasPrefix(auth, "Bearer ") || strings.HasPrefix(auth, "bearer ") {
+			return strings.TrimSpace(auth[7:])
+		}
+	}
+
+	// 2. Check x-goog-api-key header (Gemini-specific)
+	if key := c.GetHeader("x-goog-api-key"); key != "" {
+		return key
+	}
+
+	// 3. Check x-api-key header (Claude-style)
+	if key := c.GetHeader("x-api-key"); key != "" {
+		return key
+	}
+
+	// 4. Check query parameter
+	if key := c.Query("key"); key != "" {
+		return key
+	}
+
+	return ""
+}
+
+// findGeminiFileChannel finds an available Gemini channel for file operations
+func findGeminiFileChannel(c *gin.Context, userGroup string) (*model.Channel, error) {
+	// Try multiple common Gemini models to find an available channel
+	geminiModels := []string{
+		"gemini-2.0-flash",
+		"gemini-1.5-flash",
+		"gemini-1.5-pro",
+		"gemini-2.0-flash-exp",
+		"gemini-pro",
+		"gemini-1.0-pro",
+	}
+
+	var lastError error
+	for _, modelName := range geminiModels {
+		channel, _, err := service.CacheGetRandomSatisfiedChannel(&service.RetryParam{
+			Ctx:        c,
+			ModelName:  modelName,
+			TokenGroup: userGroup,
+			Retry:      common.GetPointer(0),
+		})
+
+		if err == nil && channel != nil {
+			logger.LogDebug(c, fmt.Sprintf("Found Gemini channel for file operations using model: %s", modelName))
+			return channel, nil
+		}
+		lastError = err
+	}
+
+	if lastError != nil {
+		return nil, fmt.Errorf("failed to find Gemini channel: %w", lastError)
+	}
+	return nil, fmt.Errorf("no available Gemini channel found")
+}

+ 72 - 0
middleware/gemini_file_channel.go

@@ -0,0 +1,72 @@
+package middleware
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/service"
+
+	"github.com/gin-gonic/gin"
+)
+
+// SetupGeminiFileChannel selects a Gemini channel for File API operations
+// This middleware is used instead of Distribute() for File API endpoints
+// since they don't require model-based channel selection
+func SetupGeminiFileChannel() func(c *gin.Context) {
+	return func(c *gin.Context) {
+		// Get user's group
+		usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
+		if usingGroup == "" {
+			usingGroup = common.GetContextKeyString(c, constant.ContextKeyUserGroup)
+		}
+
+		// Try multiple common Gemini models to find an available channel
+		// The actual File API doesn't require a model, but we need one to select a channel
+		geminiModels := []string{
+		"gemini-2.0-flash",
+			"gemini-1.5-flash",
+			"gemini-1.5-pro",
+			"gemini-2.0-flash-exp",
+			"gemini-pro",
+			"gemini-1.0-pro",
+		}
+
+		var channel *model.Channel
+		var err error
+		var lastError error
+
+		// Try each model until we find an available channel
+		for _, modelName := range geminiModels {
+			channel, _, err = service.CacheGetRandomSatisfiedChannel(&service.RetryParam{
+				Ctx:        c,
+				ModelName:  modelName,
+				TokenGroup: usingGroup,
+				Retry:      common.GetPointer(0),
+			})
+
+			if err == nil && channel != nil {
+				// Found a channel, setup context and continue
+				newAPIError := SetupContextForSelectedChannel(c, channel, modelName)
+				if newAPIError != nil {
+					abortWithOpenAiMessage(c, http.StatusServiceUnavailable,
+						fmt.Sprintf("设置 Gemini 渠道失败: %s", newAPIError.Error()))
+					return
+				}
+				c.Next()
+				return
+			}
+			lastError = err
+		}
+
+		// No channel found with any of the models
+		errorMsg := "没有可用的 Gemini 文件 API 渠道"
+		if lastError != nil {
+			errorMsg = fmt.Sprintf("获取 Gemini 文件 API 渠道失败: %s", lastError.Error())
+		}
+		abortWithOpenAiMessage(c, http.StatusServiceUnavailable, errorMsg)
+	}
+}
+

+ 29 - 0
router/gemini_file_router.go

@@ -0,0 +1,29 @@
+package router
+
+import (
+	"github.com/QuantumNous/new-api/controller"
+	"github.com/QuantumNous/new-api/middleware"
+
+	"github.com/gin-gonic/gin"
+)
+
+// SetGeminiFileRouter sets up routes for Gemini File API operations
+// This is completely isolated from other API routes to avoid interference
+func SetGeminiFileRouter(router *gin.Engine) {
+	// Gemini File API routes - completely independent
+	geminiFileRouter := router.Group("/v1beta")
+	geminiFileRouter.Use(middleware.CORS())
+	geminiFileRouter.Use(middleware.GeminiFileAuth())
+	{
+		// File list endpoint
+		geminiFileRouter.GET("/files", controller.RelayGeminiFileList)
+	}
+
+	// File upload endpoint - separate path prefix
+	geminiFileUploadRouter := router.Group("/upload/v1beta")
+	geminiFileUploadRouter.Use(middleware.CORS())
+	geminiFileUploadRouter.Use(middleware.GeminiFileAuth())
+	{
+		geminiFileUploadRouter.POST("/files", controller.RelayGeminiFileUpload)
+	}
+}

+ 2 - 0
router/main.go

@@ -17,6 +17,8 @@ func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
 	SetDashboardRouter(router)
 	SetRelayRouter(router)
 	SetVideoRouter(router)
+	// Gemini File API - completely isolated module
+	SetGeminiFileRouter(router)
 	frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL")
 	if common.IsMasterNode && frontendBaseUrl != "" {
 		frontendBaseUrl = ""

+ 8 - 21
router/relay-router.go

@@ -22,8 +22,8 @@ func SetRelayRouter(router *gin.Engine) {
 			switch {
 			case c.GetHeader("x-api-key") != "" && c.GetHeader("anthropic-version") != "":
 				controller.ListModels(c, constant.ChannelTypeAnthropic)
-			case c.GetHeader("x-goog-api-key") != "" || c.Query("key") != "": // 单独的适配
-				controller.RetrieveModel(c, constant.ChannelTypeGemini)
+			case c.GetHeader("x-goog-api-key") != "":
+				controller.ListModels(c, constant.ChannelTypeGemini)
 			default:
 				controller.ListModels(c, constant.ChannelTypeOpenAI)
 			}
@@ -169,30 +169,17 @@ func SetRelayRouter(router *gin.Engine) {
 		relaySunoRouter.GET("/fetch/:id", controller.RelayTask)
 	}
 
-	relayGeminiRouter := router.Group("/v1beta")
-	relayGeminiRouter.Use(middleware.TokenAuth())
-	relayGeminiRouter.Use(middleware.ModelRequestRateLimit())
-	relayGeminiRouter.Use(middleware.Distribute())
+	// Gemini Model API routes (with Distribute middleware)
+	relayGeminiModelRouter := router.Group("/v1beta")
+	relayGeminiModelRouter.Use(middleware.TokenAuth())
+	relayGeminiModelRouter.Use(middleware.ModelRequestRateLimit())
+	relayGeminiModelRouter.Use(middleware.Distribute())
 	{
-		// Gemini File API routes
-		relayGeminiRouter.GET("/files", controller.RelayGeminiFileList)
-		relayGeminiRouter.GET("/files/*name", controller.RelayGeminiFileGet)
-		relayGeminiRouter.DELETE("/files/*name", controller.RelayGeminiFileDelete)
-
 		// Gemini API 路径格式: /v1beta/models/{model_name}:{action}
-		relayGeminiRouter.POST("/models/*path", func(c *gin.Context) {
+		relayGeminiModelRouter.POST("/models/*path", func(c *gin.Context) {
 			controller.Relay(c, types.RelayFormatGemini)
 		})
 	}
-
-	// Gemini File Upload route (separate group for different path prefix)
-	relayGeminiUploadRouter := router.Group("/upload/v1beta")
-	relayGeminiUploadRouter.Use(middleware.TokenAuth())
-	relayGeminiUploadRouter.Use(middleware.ModelRequestRateLimit())
-	relayGeminiUploadRouter.Use(middleware.Distribute())
-	{
-		relayGeminiUploadRouter.POST("/files", controller.RelayGeminiFileUpload)
-	}
 }
 
 func registerMjRouterGroup(relayMjRouter *gin.RouterGroup) {