diff --git a/README.md b/README.md index 370b1f1..64ce23e 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ ## 📝 项目简介 -本项目是一个基于 FastAPI 框架开发的高性能、易于部署的 OpenAI 和 Gemini API 代理服务。它不仅兼容 OpenAI 的 API 接口,还支持 Google 的 Gemini 模型,为用户提供灵活的模型选择。该代理服务内置了多 API Key 轮询、负载均衡、自动重试、访问控制(Bearer Token 认证)、流式响应等功能,旨在简化 AI 应用的开发和部署流程。 +本项目是一个基于 FastAPI 框架开发的高性能、易于部署的Gemini OpenAI兼容 和 Gemini API 代理服务。它不仅兼容 OpenAI 的 API 接口,还支持 Google 的 Gemini 原生接口。该代理服务内置了多 API Key 轮询、负载均衡、自动重试、访问控制(Bearer Token 认证)、流式响应等功能,旨在简化 AI 应用的开发和部署流程。 **核心功能与优势:** -- **多模型支持**: 无缝切换 OpenAI 和 Gemini 模型。 +- **多协议支持**: 无缝切换 OpenAI兼容 和 Gemini 协议。 - **智能 API Key 管理**: 自动轮询多个 API Key,实现负载均衡和故障转移。 - **安全访问控制**: 使用 Bearer Token 进行身份验证,保护 API 访问。 - **流式响应支持**: 提供实时的流式数据传输,提升用户体验。 @@ -170,13 +170,30 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload 所有 API 请求都需要在 Header 中添加 `Authorization` 字段,值为 `Bearer `,其中 `` 需要替换为你在 `.env` 文件中配置的 `ALLOWED_TOKENS` 中的一个或者 `AUTH_TOKEN`。 -### 获取模型列表 +### API 路由 + +本服务提供两种API路由: + +1. **OpenAI 兼容路由** (推荐) + - 基础路径: `/v1` + - 完全兼容OpenAI API格式 + - 支持所有Gemini模型 + +2. **Gemini 原生路由** + - 基础路径: `/gemini/v1beta` 或 `/v1beta` + - 遵循Google原生API格式 + - 适用于需要直接使用Gemini API的场景 + +### OpenAI兼容路由 + +#### 获取模型列表 - **URL**: `/v1/models` - **Method**: `GET` - **Header**: `Authorization: Bearer ` +- **Response**: 返回支持的所有模型列表,包括最新的`gemini-2.0-flash-exp-search`等模型 -### 聊天补全 (Chat Completions) +#### 聊天补全 (Chat Completions) - **URL**: `/v1/chat/completions` - **Method**: `POST` @@ -202,11 +219,34 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload } ``` - - `messages`: 消息列表,格式与 OpenAI API 相同。 - - `model`: 模型名称,例如 `gemini-1.5-flash-002`。 - - `stream`: 是否开启流式响应,`true` 或 `false`。 - - `tools`: 使用的工具列表。 - - 其他参数:与 OpenAI API 兼容的参数,如 `temperature`, `max_tokens` 等。 + - `messages`: 消息列表,格式与 OpenAI API 相同 + - `model`: 模型名称,支持所有Gemini模型,包括: + - `gemini-1.5-flash-002`: 快速响应模型 + - `gemini-2.0-flash-exp`: 实验性快速响应模型 + - `gemini-2.0-flash-exp-search`: 支持搜索功能的实验性模型 + - `stream`: 是否开启流式响应,`true` 或 `false` + - `tools`: 使用的工具列表 + - 其他参数:与 OpenAI API 兼容的参数,如 `temperature`, `max_tokens` 等 + +### Gemini原生路由 + +#### 获取模型列表 + +- **URL**: `/gemini/v1beta/models` 或 `/v1beta/models` +- **Method**: `GET` +- **Header**: `Authorization: Bearer ` + +#### 生成内容 + +- **URL**: `/gemini/v1beta/models/{model_name}:generateContent` +- **Method**: `POST` +- **Header**: `Authorization: Bearer ` + +#### 流式生成内容 + +- **URL**: `/gemini/v1beta/models/{model_name}:streamGenerateContent` +- **Method**: `POST` +- **Header**: `Authorization: Bearer ` ### 获取词向量 (Embeddings) @@ -230,12 +270,30 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload - **URL**: `/health` - **Method**: `GET` -### 获取 API Key 列表 +### Web界面功能 + +#### 验证页面 + +- **URL**: `/auth` +- **说明**: 提供了一个简洁的Web界面用于验证访问令牌 +- **功能**: + - 美观的用户界面,支持响应式设计 + - 安全的令牌验证机制 + - 错误提示功能 + - 支持移动端访问 + +#### API密钥状态管理 - **URL**: `/v1/keys/list` - **Method**: `GET` - **Header**: `Authorization: Bearer ` -- **说明**: 只有使用 `AUTH_TOKEN` 才能访问此接口, 用于获取有效和无效的 API Key 列表。 +- **说明**: + - 只有使用 `AUTH_TOKEN` 才能访问此接口 + - 提供了可视化的Web界面展示API密钥状态 + - 支持查看有效和无效的API密钥列表 + - 显示每个密钥的失败次数统计 + - 提供一键复制功能(支持复制单个密钥或批量复制) + - 实时显示密钥总数统计 ### 图片生成 (Image Generation) diff --git a/app/api/gemini_routes.py b/app/api/gemini_routes.py index d16cd35..b593c48 100644 --- a/app/api/gemini_routes.py +++ b/app/api/gemini_routes.py @@ -6,7 +6,7 @@ from app.core.logger import get_gemini_logger from app.core.security import SecurityService from app.schemas.gemini_models import GeminiRequest from app.services.gemini_chat_service import GeminiChatService -from app.services.key_manager import KeyManager +from app.services.key_manager import KeyManager, get_key_manager_instance from app.services.model_service import ModelService from app.services.chat.retry_handler import RetryHandler @@ -16,13 +16,20 @@ logger = get_gemini_logger() # 初始化服务 security_service = SecurityService(settings.ALLOWED_TOKENS, settings.AUTH_TOKEN) -key_manager = KeyManager(settings.API_KEYS) + +async def get_key_manager(): + return await get_key_manager_instance() + +async def get_next_working_key_wrapper(key_manager: KeyManager = Depends(get_key_manager)): + return await key_manager.get_next_working_key() + model_service = ModelService(settings.MODEL_SEARCH) @router.get("/models") @router_v1beta.get("/models") -async def list_models(_=Depends(security_service.verify_key)): +async def list_models(_=Depends(security_service.verify_key), + key_manager: KeyManager = Depends(get_key_manager)): """获取可用的Gemini模型列表""" logger.info("-" * 50 + "list_gemini_models" + "-" * 50) logger.info("Handling Gemini models list request") @@ -40,12 +47,13 @@ async def list_models(_=Depends(security_service.verify_key)): @router.post("/models/{model_name}:generateContent") @router_v1beta.post("/models/{model_name}:generateContent") -@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key") +@RetryHandler(max_retries=3, key_manager=Depends(get_key_manager), key_arg="api_key") async def generate_content( model_name: str, request: GeminiRequest, _=Depends(security_service.verify_goog_api_key), - api_key: str = Depends(key_manager.get_next_working_key), + api_key: str = Depends(get_next_working_key_wrapper), + key_manager: KeyManager = Depends(get_key_manager) ): chat_service = GeminiChatService(settings.BASE_URL, key_manager) """非流式生成内容""" @@ -69,12 +77,13 @@ async def generate_content( @router.post("/models/{model_name}:streamGenerateContent") @router_v1beta.post("/models/{model_name}:streamGenerateContent") -@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key") +@RetryHandler(max_retries=3, key_manager=Depends(get_key_manager), key_arg="api_key") async def stream_generate_content( model_name: str, request: GeminiRequest, _=Depends(security_service.verify_goog_api_key), - api_key: str = Depends(key_manager.get_next_working_key), + api_key: str = Depends(get_next_working_key_wrapper), + key_manager: KeyManager = Depends(get_key_manager) ): chat_service = GeminiChatService(settings.BASE_URL, key_manager) """流式生成内容""" diff --git a/app/api/openai_routes.py b/app/api/openai_routes.py index 9982ea8..0d94e42 100644 --- a/app/api/openai_routes.py +++ b/app/api/openai_routes.py @@ -8,7 +8,7 @@ from app.schemas.openai_models import ChatRequest, EmbeddingRequest, ImageGenera from app.services.chat.retry_handler import RetryHandler from app.services.embedding_service import EmbeddingService from app.services.image_create_service import ImageCreateService -from app.services.key_manager import KeyManager +from app.services.key_manager import KeyManager, get_key_manager_instance from app.services.model_service import ModelService from app.services.openai_chat_service import OpenAIChatService @@ -17,15 +17,22 @@ logger = get_openai_logger() # 初始化服务 security_service = SecurityService(settings.ALLOWED_TOKENS, settings.AUTH_TOKEN) -key_manager = KeyManager(settings.API_KEYS) model_service = ModelService(settings.MODEL_SEARCH) embedding_service = EmbeddingService(settings.BASE_URL) image_create_service = ImageCreateService() +async def get_key_manager(): + return await get_key_manager_instance() + +async def get_next_working_key_wrapper(key_manager: KeyManager = Depends(get_key_manager)): + return await key_manager.get_next_working_key() @router.get("/v1/models") @router.get("/hf/v1/models") -async def list_models(_=Depends(security_service.verify_authorization)): +async def list_models( + _=Depends(security_service.verify_authorization), + key_manager: KeyManager = Depends(get_key_manager) +): logger.info("-" * 50 + "list_models" + "-" * 50) logger.info("Handling models list request") api_key = await key_manager.get_next_working_key() @@ -39,11 +46,12 @@ async def list_models(_=Depends(security_service.verify_authorization)): @router.post("/v1/chat/completions") @router.post("/hf/v1/chat/completions") -@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key") +@RetryHandler(max_retries=3, key_manager=Depends(get_key_manager), key_arg="api_key") async def chat_completion( - request: ChatRequest, - _=Depends(security_service.verify_authorization), - api_key: str = Depends(key_manager.get_next_working_key), + request: ChatRequest, + _=Depends(security_service.verify_authorization), + api_key: str = Depends(get_next_working_key_wrapper), + key_manager: KeyManager = Depends(get_key_manager) ): # 如果model是imagen3,使用paid_key if request.model == f"{settings.CREATE_IMAGE_MODEL}-chat": @@ -58,23 +66,21 @@ async def chat_completion( if request.model == f"{settings.CREATE_IMAGE_MODEL}-chat": response = await chat_service.create_image_chat_completion(request=request) else: - response = await chat_service.create_chat_completion(request,api_key) + response = await chat_service.create_chat_completion(request, api_key) # 处理流式响应 if request.stream: return StreamingResponse(response, media_type="text/event-stream") logger.info("Chat completion request successful") return response - except Exception as e: logger.error(f"Chat completion failed after retries: {str(e)}") raise HTTPException(status_code=500, detail="Chat completion failed") from e - @router.post("/v1/images/generations") @router.post("/hf/v1/images/generations") async def generate_image( - request: ImageGenerationRequest, - _=Depends(security_service.verify_authorization), + request: ImageGenerationRequest, + _=Depends(security_service.verify_authorization), ): logger.info("-" * 50 + "generate_image" + "-" * 50) logger.info(f"Handling image generation request for prompt: {request.prompt}") @@ -83,17 +89,16 @@ async def generate_image( response = image_create_service.generate_images(request) logger.info("Image generation request successful") return response - except Exception as e: logger.error(f"Image generation request failed: {str(e)}") raise HTTPException(status_code=500, detail="Image generation request failed") from e - @router.post("/v1/embeddings") @router.post("/hf/v1/embeddings") async def embedding( - request: EmbeddingRequest, - _=Depends(security_service.verify_authorization), + request: EmbeddingRequest, + _=Depends(security_service.verify_authorization), + key_manager: KeyManager = Depends(get_key_manager) ): logger.info("-" * 50 + "embedding" + "-" * 50) logger.info(f"Handling embedding request for model: {request.model}") @@ -109,11 +114,11 @@ async def embedding( logger.error(f"Embedding request failed: {str(e)}") raise HTTPException(status_code=500, detail="Embedding request failed") from e - @router.get("/v1/keys/list") @router.get("/hf/v1/keys/list") async def get_keys_list( - _=Depends(security_service.verify_auth_token), + _=Depends(security_service.verify_auth_token), + key_manager: KeyManager = Depends(get_key_manager) ): """获取有效和无效的API key列表""" logger.info("-" * 50 + "get_keys_list" + "-" * 50) diff --git a/app/core/security.py b/app/core/security.py index 4e80c4d..e4c78e9 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -1,9 +1,12 @@ from fastapi import HTTPException, Header from typing import Optional from app.core.logger import get_security_logger +from app.core.config import settings logger = get_security_logger() +def verify_auth_token(token: str) -> bool: + return token == settings.AUTH_TOKEN class SecurityService: def __init__(self, allowed_tokens: list, auth_token: str): diff --git a/app/main.py b/app/main.py index 85bc6d9..b232b65 100644 --- a/app/main.py +++ b/app/main.py @@ -1,17 +1,53 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles from app.core.logger import get_main_logger +from app.core.security import verify_auth_token +from app.services.key_manager import get_key_manager_instance +from app.core.config import settings from app.api import gemini_routes, openai_routes import uvicorn -from app.middleware.request_logging_middleware import RequestLoggingMiddleware # 配置日志 logger = get_main_logger() app = FastAPI() +# 配置Jinja2模板 +templates = Jinja2Templates(directory="app/templates") + +# 创建 KeyManager 实例 +key_manager = None + +@app.on_event("startup") +async def startup_event(): + global key_manager + key_manager = await get_key_manager_instance(settings.API_KEYS) + +# 配置静态文件 +app.mount("/static", StaticFiles(directory="app/static"), name="static") + +# 添加中间件来处理未经身份验证的请求 +@app.middleware("http") +async def auth_middleware(request: Request, call_next): + # 允许 gemini_routes 和 openai_routes 中的端点绕过身份验证 + if (request.url.path not in ["/", "/auth"] and + not request.url.path.startswith("/static") and + not request.url.path.startswith("/gemini") and + not request.url.path.startswith("/v1") and + not request.url.path.startswith("/v1beta") and + not request.url.path.startswith("/health") and + not request.url.path.startswith("/hf")): + auth_token = request.cookies.get("auth_token") + if not auth_token or not verify_auth_token(auth_token): + return RedirectResponse(url="/") + response = await call_next(request) + return response + # 添加请求日志中间件 # app.add_middleware(RequestLoggingMiddleware) @@ -32,12 +68,49 @@ app.include_router(gemini_routes.router) app.include_router(gemini_routes.router_v1beta) +@app.get("/", response_class=HTMLResponse) +async def auth_page(request: Request): + return templates.TemplateResponse("auth.html", {"request": request}) + + +@app.post("/auth") +async def authenticate(request: Request): + try: + form = await request.form() + auth_token = form.get("auth_token") + if not auth_token: + return RedirectResponse(url="/", status_code=302) + + if verify_auth_token(auth_token): + response = RedirectResponse(url="/keys", status_code=302) + response.set_cookie(key="auth_token", value=auth_token, httponly=True, max_age=3600) + return response + return RedirectResponse(url="/", status_code=302) + except Exception as e: + logger.error(f"Authentication error: {str(e)}") + return RedirectResponse(url="/", status_code=302) + + +@app.get("/keys", response_class=HTMLResponse) +async def keys_page(request: Request): + auth_token = request.cookies.get("auth_token") + if not auth_token or not verify_auth_token(auth_token): + return RedirectResponse(url="/", status_code=302) + keys_status = await key_manager.get_keys_by_status() + total = len(keys_status["valid_keys"]) + len(keys_status["invalid_keys"]) + return templates.TemplateResponse("keys_status.html", { + "request": request, + "valid_keys": keys_status["valid_keys"], + "invalid_keys": keys_status["invalid_keys"], + "total": total + }) + + @app.get("/health") -@app.get("/") -async def health_check(): +async def health_check(request: Request): logger.info("Health check endpoint called") return {"status": "healthy"} - + if __name__ == "__main__": diff --git a/app/services/key_manager.py b/app/services/key_manager.py index 3e474c3..6895fbe 100644 --- a/app/services/key_manager.py +++ b/app/services/key_manager.py @@ -4,6 +4,7 @@ from typing import Dict from app.core.logger import get_key_manager_logger from app.core.config import settings + logger = get_key_manager_logger() @@ -61,20 +62,44 @@ class KeyManager: return await self.get_next_working_key() + def get_fail_count(self, key: str) -> int: + """获取指定密钥的失败次数""" + return self.key_failure_counts.get(key, 0) + async def get_keys_by_status(self) -> dict: - """获取分类后的API key列表""" - valid_keys = [] - invalid_keys = [] + """获取分类后的API key列表,包括失败次数""" + valid_keys = {} + invalid_keys = {} async with self.failure_count_lock: for key in self.api_keys: - masked_key = f"{key}" - if self.key_failure_counts[key] < self.MAX_FAILURES: - valid_keys.append(masked_key) + fail_count = self.key_failure_counts[key] + if fail_count < self.MAX_FAILURES: + valid_keys[key] = fail_count else: - invalid_keys.append(masked_key) + invalid_keys[key] = fail_count return { "valid_keys": valid_keys, "invalid_keys": invalid_keys } + + +_singleton_instance = None +_singleton_lock = asyncio.Lock() + +async def get_key_manager_instance(api_keys: list = None) -> KeyManager: + """ + 获取 KeyManager 单例实例。 + + 如果尚未创建实例,将使用提供的 api_keys 初始化 KeyManager。 + 如果已创建实例,则忽略 api_keys 参数,返回现有单例。 + """ + global _singleton_instance + + async with _singleton_lock: + if _singleton_instance is None: + if api_keys is None: + raise ValueError("API keys are required to initialize the KeyManager") + _singleton_instance = KeyManager(api_keys) + return _singleton_instance diff --git a/app/templates/auth.html b/app/templates/auth.html new file mode 100644 index 0000000..abfcc1e --- /dev/null +++ b/app/templates/auth.html @@ -0,0 +1,89 @@ + + + + + + 验证页面 + + + + +
+

请输入验证令牌

+
+ + +
+ {% if error %} +

{{ error }}

+ {% endif %} +
+ + diff --git a/app/templates/keys_status.html b/app/templates/keys_status.html new file mode 100644 index 0000000..2ec0438 --- /dev/null +++ b/app/templates/keys_status.html @@ -0,0 +1,214 @@ + + + + + + API密钥状态 + + + + +
+

API密钥状态

+
+

+ 有效密钥 + +

+
    + {% for key, fail_count in valid_keys.items() %} +
  • + {{ key }} + (失败次数: {{ fail_count }}) + +
  • + {% endfor %} +
+
+
+

+ 无效密钥 + +

+
    + {% for key, fail_count in invalid_keys.items() %} +
  • + {{ key }} + (失败次数: {{ fail_count }}) + +
  • + {% endfor %} +
+
+
+ 总密钥数:{{ total }} +
+
+
+ + + + diff --git a/requirements.txt b/requirements.txt index fd46e65..6bb809c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ pydantic_settings requests starlette uvicorn -google-genai \ No newline at end of file +google-genai +jinja2