diff --git a/app/domain/gemini_models.py b/app/domain/gemini_models.py index 9528bc8..54392e2 100644 --- a/app/domain/gemini_models.py +++ b/app/domain/gemini_models.py @@ -45,7 +45,7 @@ class GenerationConfig(BaseModel): class SystemInstruction(BaseModel): role: str = "system" - parts: List[Dict[str, Any]] | Dict[str, Any] + parts: Union[List[Dict[str, Any]], Dict[str, Any]] class GeminiContent(BaseModel): diff --git a/app/domain/image_models.py b/app/domain/image_models.py index 02bf3f6..29875cc 100644 --- a/app/domain/image_models.py +++ b/app/domain/image_models.py @@ -1,23 +1,20 @@ +from typing import Union + + class ImageMetadata: - def __init__(self, width: int, height: int, filename: str, size: int, url: str, delete_url: str | None = None): + def __init__(self, width: int, height: int, filename: str, size: int, url: str, delete_url: Union[str, None] = None): self.width = width self.height = height self.filename = filename self.size = size self.url = url self.delete_url = delete_url - - class UploadResponse: def __init__(self, success: bool, code: str, message: str, data: ImageMetadata): self.success = success self.code = code self.message = message self.data = data - - class ImageUploader: def upload(self, file: bytes, filename: str) -> UploadResponse: raise NotImplementedError - - diff --git a/app/service/key/key_manager.py b/app/service/key/key_manager.py index 827ead3..94b9ae6 100644 --- a/app/service/key/key_manager.py +++ b/app/service/key/key_manager.py @@ -1,6 +1,6 @@ import asyncio from itertools import cycle -from typing import Dict +from typing import Dict, Union from app.config.config import settings from app.log.logger import get_key_manager_logger @@ -178,19 +178,20 @@ class KeyManager: if self.api_keys: return self.api_keys[0] if not self.api_keys: - logger.warning("API key list is empty, cannot get first valid key.") + logger.warning( + "API key list is empty, cannot get first valid key.") return "" return self.api_keys[0] _singleton_instance = None _singleton_lock = asyncio.Lock() -_preserved_failure_counts: Dict[str, int] | None = None -_preserved_vertex_failure_counts: Dict[str, int] | None = None -_preserved_old_api_keys_for_reset: list | None = None -_preserved_vertex_old_api_keys_for_reset: list | None = None -_preserved_next_key_in_cycle: str | None = None -_preserved_vertex_next_key_in_cycle: str | None = None +_preserved_failure_counts: Union[Dict[str, int], None] = None +_preserved_vertex_failure_counts: Union[Dict[str, int], None] = None +_preserved_old_api_keys_for_reset: Union[list, None] = None +_preserved_vertex_old_api_keys_for_reset: Union[list, None] = None +_preserved_next_key_in_cycle: Union[str, None] = None +_preserved_vertex_next_key_in_cycle: Union[str, None] = None async def get_key_manager_instance( @@ -252,7 +253,8 @@ async def get_key_manager_instance( _singleton_instance.vertex_key_failure_counts = ( current_vertex_failure_counts ) - logger.info("Inherited failure counts for applicable Vertex keys.") + logger.info( + "Inherited failure counts for applicable Vertex keys.") _preserved_vertex_failure_counts = None # 2. 调整 key_cycle 的起始点 @@ -357,7 +359,7 @@ async def get_key_manager_instance( f"Error determining start key for new Vertex key cycle from preserved state: {e}. " "New cycle will start from the beginning." ) - + if start_key_for_new_vertex_cycle and _singleton_instance.vertex_api_keys: try: target_idx = _singleton_instance.vertex_api_keys.index( @@ -380,7 +382,7 @@ async def get_key_manager_instance( except Exception as e: logger.error( f"Error advancing new Vertex key cycle: {e}. Cycle will start from beginning." - ) + ) else: if _singleton_instance.vertex_api_keys: logger.info( @@ -418,7 +420,7 @@ async def reset_key_manager_instance(): # 3. 保存 key_cycle 的下一个 key 提示 try: if _singleton_instance.api_keys: - _preserved_next_key_in_cycle = ( + _preserved_next_key_in_cycle = ( await _singleton_instance.get_next_key() ) else: @@ -427,9 +429,10 @@ async def reset_key_manager_instance(): logger.warning( "Could not preserve next key hint: key cycle was empty or exhausted in old instance." ) - _preserved_next_key_in_cycle = None + _preserved_next_key_in_cycle = None except Exception as e: - logger.error(f"Error preserving next key hint during reset: {e}") + logger.error( + f"Error preserving next key hint during reset: {e}") _preserved_next_key_in_cycle = None # 4. 保存 vertex_key_cycle 的下一个 key 提示 @@ -443,12 +446,12 @@ async def reset_key_manager_instance(): except StopIteration: logger.warning( "Could not preserve next key hint: Vertex key cycle was empty or exhausted in old instance." - ) + ) _preserved_vertex_next_key_in_cycle = None except Exception as e: - logger.error(f"Error preserving next key hint during reset: {e}") + logger.error( + f"Error preserving next key hint during reset: {e}") _preserved_vertex_next_key_in_cycle = None - _singleton_instance = None logger.info( diff --git a/app/service/stats/stats_service.py b/app/service/stats/stats_service.py index 3bc9a93..e371ad8 100644 --- a/app/service/stats/stats_service.py +++ b/app/service/stats/stats_service.py @@ -1,6 +1,7 @@ # app/service/stats_service.py import datetime +from typing import Union from sqlalchemy import and_, case, func, or_, select @@ -195,10 +196,11 @@ class StatsService: return details except Exception as e: - logger.error(f"Failed to get API call details for period '{period}': {e}") + logger.error( + f"Failed to get API call details for period '{period}': {e}") raise - async def get_key_usage_details_last_24h(self, key: str) -> dict | None: + async def get_key_usage_details_last_24h(self, key: str) -> Union[dict, None]: """ 获取指定 API 密钥在过去 24 小时内按模型统计的调用次数。 @@ -218,7 +220,8 @@ class StatsService: try: query = ( select( - RequestLog.model_name, func.count(RequestLog.id).label("call_count") + RequestLog.model_name, func.count( + RequestLog.id).label("call_count") ) .where( RequestLog.api_key == key, @@ -237,7 +240,8 @@ class StatsService: ) return {} - usage_details = {row["model_name"]: row["call_count"] for row in results} + usage_details = {row["model_name"]: row["call_count"] + for row in results} logger.info( f"Successfully fetched usage details for key ending in ...{key[-4:]}: {usage_details}" ) diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js index b2c4467..9c8de79 100644 --- a/app/static/js/config_editor.js +++ b/app/static/js/config_editor.js @@ -12,7 +12,7 @@ const PROXY_REGEX = /(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g; const VERTEX_API_KEY_REGEX = /AQ\.[a-zA-Z0-9_]{50}/g; // 新增 Vertex API Key 正则 const MASKED_VALUE = "••••••••"; - + // DOM Elements - Global Scope for frequently accessed elements const safetySettingsContainer = document.getElementById( "SAFETY_SETTINGS_container" @@ -31,7 +31,7 @@ const bulkDeleteProxyModal = document.getElementById("bulkDeleteProxyModal"); const bulkDeleteProxyInput = document.getElementById("bulkDeleteProxyInput"); const resetConfirmModal = document.getElementById("resetConfirmModal"); const configForm = document.getElementById("configForm"); // Added for frequent use - + // Vertex API Key Modal Elements const vertexApiKeyModal = document.getElementById("vertexApiKeyModal"); const vertexApiKeyBulkInput = document.getElementById("vertexApiKeyBulkInput"); @@ -41,7 +41,7 @@ const bulkDeleteVertexApiKeyModal = document.getElementById( const bulkDeleteVertexApiKeyInput = document.getElementById( "bulkDeleteVertexApiKeyInput" ); - + // Model Helper Modal Elements const modelHelperModal = document.getElementById("modelHelperModal"); const modelHelperTitleElement = document.getElementById("modelHelperTitle"); @@ -384,7 +384,7 @@ document.addEventListener("DOMContentLoaded", function () { } initializeSensitiveFields(); // Initialize sensitive field handling - + // Vertex API Key Modal Elements and Events const addVertexApiKeyBtn = document.getElementById("addVertexApiKeyBtn"); const closeVertexApiKeyModalBtn = document.getElementById( @@ -408,7 +408,7 @@ document.addEventListener("DOMContentLoaded", function () { const confirmBulkDeleteVertexApiKeyBtn = document.getElementById( "confirmBulkDeleteVertexApiKeyBtn" ); - + if (addVertexApiKeyBtn) { addVertexApiKeyBtn.addEventListener("click", () => { openModal(vertexApiKeyModal); @@ -428,7 +428,7 @@ document.addEventListener("DOMContentLoaded", function () { "click", handleBulkAddVertexApiKeys ); - + if (bulkDeleteVertexApiKeyBtn) { bulkDeleteVertexApiKeyBtn.addEventListener("click", () => { openModal(bulkDeleteVertexApiKeyModal); @@ -448,7 +448,7 @@ document.addEventListener("DOMContentLoaded", function () { "click", handleBulkDeleteVertexApiKeys ); - + // Model Helper Modal Event Listeners if (closeModelHelperModalBtn) { closeModelHelperModalBtn.addEventListener("click", () => @@ -765,7 +765,7 @@ async function initConfig() { FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS: 5, // --- 结束:处理假流式配置的默认值 --- }; - + populateForm(defaultConfig); if (configForm) { // Ensure form exists @@ -1177,7 +1177,7 @@ function handleBulkDeleteProxies() { } bulkDeleteProxyInput.value = ""; } - + /** * Handles the bulk addition of Vertex API keys from the modal input. */ @@ -1192,10 +1192,10 @@ function handleBulkAddVertexApiKeys() { ) { return; } - + const bulkText = vertexApiKeyBulkInput.value; const extractedKeys = bulkText.match(VERTEX_API_KEY_REGEX) || []; - + const currentKeyInputs = vertexApiKeyContainer.querySelectorAll( `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` ); @@ -1206,16 +1206,16 @@ function handleBulkAddVertexApiKeys() { : input.value; }) .filter((key) => key && key.trim() !== "" && key !== MASKED_VALUE); - + const combinedKeys = new Set([...currentKeys, ...extractedKeys]); const uniqueKeys = Array.from(combinedKeys); - + vertexApiKeyContainer.innerHTML = ""; // Clear existing items - + uniqueKeys.forEach((key) => { addArrayItemWithValue("VERTEX_API_KEYS", key); // VERTEX_API_KEYS are sensitive }); - + // Ensure new sensitive inputs are masked const newKeyInputs = vertexApiKeyContainer.querySelectorAll( `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` @@ -1229,7 +1229,7 @@ function handleBulkAddVertexApiKeys() { input.dispatchEvent(focusoutEvent); } }); - + closeModal(vertexApiKeyModal); showNotification( `添加/更新了 ${uniqueKeys.length} 个唯一 Vertex 密钥`, @@ -1237,7 +1237,7 @@ function handleBulkAddVertexApiKeys() { ); vertexApiKeyBulkInput.value = ""; } - + /** * Handles the bulk deletion of Vertex API keys based on input from the modal. */ @@ -1252,15 +1252,15 @@ function handleBulkDeleteVertexApiKeys() { ) { return; } - + const bulkText = bulkDeleteVertexApiKeyInput.value; if (!bulkText.trim()) { showNotification("请粘贴需要删除的 Vertex API 密钥", "warning"); return; } - + const keysToDelete = new Set(bulkText.match(VERTEX_API_KEY_REGEX) || []); - + if (keysToDelete.size === 0) { showNotification( "未在输入内容中提取到有效的 Vertex API 密钥格式", @@ -1268,10 +1268,10 @@ function handleBulkDeleteVertexApiKeys() { ); return; } - + const keyItems = vertexApiKeyContainer.querySelectorAll(`.${ARRAY_ITEM_CLASS}`); let deleteCount = 0; - + keyItems.forEach((item) => { const input = item.querySelector( `.${ARRAY_INPUT_CLASS}.${SENSITIVE_INPUT_CLASS}` @@ -1286,9 +1286,9 @@ function handleBulkDeleteVertexApiKeys() { deleteCount++; } }); - + closeModal(bulkDeleteVertexApiKeyModal); - + if (deleteCount > 0) { showNotification(`成功删除了 ${deleteCount} 个匹配的 Vertex 密钥`, "success"); } else { @@ -1296,7 +1296,7 @@ function handleBulkDeleteVertexApiKeys() { } bulkDeleteVertexApiKeyInput.value = ""; } - + /** * Switches the active configuration tab. * @param {string} tabId - The ID of the tab to switch to. @@ -1442,7 +1442,7 @@ function addArrayItemWithValue(key, value) { const isSensitive = key === "API_KEYS" || isAllowedToken || isVertexApiKey; // 更新敏感判断 const modelId = isThinkingModel ? generateUUID() : null; - + const arrayItem = document.createElement("div"); arrayItem.className = `${ARRAY_ITEM_CLASS} flex items-center mb-2 gap-2`; if (isThinkingModel) { @@ -1535,14 +1535,14 @@ function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) { valueInput.value = isNaN(intValue) ? 0 : intValue; valueInput.placeholder = "预算 (整数)"; valueInput.className = `${MAP_VALUE_INPUT_CLASS} w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50`; - valueInput.min = 0; - valueInput.max = 24576; + valueInput.min = -1; + valueInput.max = 32767; valueInput.addEventListener("input", function () { - let val = this.value.replace(/[^0-9]/g, ""); + let val = this.value.replace(/[^0-9-]/g, ""); if (val !== "") { val = parseInt(val, 10); - if (val < 0) val = 0; - if (val > 24576) val = 24576; + if (val < -1) val = -1; + if (val > 32767) val = 32767; } this.value = val; // Corrected variable name }); diff --git a/app/templates/config_editor.html b/app/templates/config_editor.html index b07081e..cb82fc8 100644 --- a/app/templates/config_editor.html +++ b/app/templates/config_editor.html @@ -1,941 +1,941 @@ {% extends "base.html" %} {% block title %}配置编辑器 - Gemini Balance{% - endblock %} {% block head_extra_styles %} - +{% endblock %} {% block content %} +
+
- {% endblock %} {% block content %} -
-
+ + +

+ Gemini Balance Logo + Gemini Balance - 配置编辑 +

+ + + + + +
- -

- Gemini Balance Logo - Gemini Balance - 配置编辑 -

- - - + + + + +
+ + - - -
- - - - - - -
- - - - - - -
-

API相关配置 +

+ + +
+ - API相关配置 - - - -
- + +
+
+ +
+
+ + +
+ Gemini API密钥列表,每行一个 +
+ + +
+ +
+ +
+
+ +
+ 允许访问API的令牌列表 +
+ + +
+ +
+
-
-
-
- -
-
-
- Gemini API密钥列表,每行一个
- - -
- -
- -
-
- -
- 允许访问API的令牌列表 + 用于API认证的令牌 +
+ + +
+ + + Gemini API的基础URL +
+ + +
+ +
+
- - -
- + -
-
- 用于API认证的令牌 + 删除Vertex密钥 + +
- - -
- - - Gemini API的基础URL -
- - -
- -
- -
-
- - -
- Vertex AI Platform API密钥列表。点击按钮可批量添加或删除。 -
- - -
- - - Vertex Express API的基础URL -
- + Vertex AI Platform API密钥列表。点击按钮可批量添加或删除。 +
+ + +
+ + + Vertex Express API的基础URL +
+
- -
- - - API密钥失败后标记为无效的次数 -
- - -
- - - API请求的超时时间 -
- - -
- - - API请求失败后的最大重试次数 -
- -
- -
- -
-
- - -
- 代理服务器列表,支持 http 和 socks5 格式,例如: - http://user:pass@host:port 或 - socks5://host:port。点击按钮可批量添加或删除。 -
- -
-
- -
- - -
-
- 开启后,对于每一个API_KEY将根据算法从代理列表中选取同一个代理IP,防止一个API_KEY同时被多个IP访问,也同时防止了一个IP访问了过多的API_KEY。 -
-
- - -
-

+
+ - 模型相关配置 -

- - -
- -
- - -
- 用于测试API密钥的模型 + + API密钥失败后标记为无效的次数 +
+ + +
+ + + API请求的超时时间 +
+ + +
+ + + API请求失败后的最大重试次数 +
+ +
+ +
+
- - -
- + - -
- 支持图像处理的模型列表 -
- - -
- 删除代理 + + - -
- 支持搜索功能的模型列表 + 添加代理 +
- - -
- 代理服务器列表,支持 http 和 socks5 格式,例如: + http://user:pass@host:port 或 + socks5://host:port。点击按钮可批量添加或删除。 +
+ +
+
+ +
+ + +
+
+ 开启后,对于每一个API_KEY将根据算法从代理列表中选取同一个代理IP,防止一个API_KEY同时被多个IP访问,也同时防止了一个IP访问了过多的API_KEY。 +
+
+ + +
+

+ 模型相关配置 +

+ + +
+ +
+ + - -
- 需要过滤的模型列表 + +
- - -
+ 用于测试API密钥的模型 +
+ + +
+ +
+ +
+
+ + +
+ 支持图像处理的模型列表 +
+ + +
+ +
+ +
+
+ + +
+ 支持搜索功能的模型列表 +
+ + +
+ +
+ +
+
+ + +
+ 需要过滤的模型列表 +
+ + +
+ +
+ -
- - -
-
- - -
- -
- - -
-
- - -
- -
- - -
-
- - -
- -
- -
-
- - -
- 用于"思考过程"的模型列表 -
- - -
- -
- -
- 请先在上方添加思考模型,然后在此处配置预算。 -
-
- - - 为每个思考模型设置预算(整数,最大值 - 24576),此项与上方模型列表自动关联。 -
- -
- -
- -
- 定义模型的安全过滤阈值。 -
-
-
- -
- 配置模型的安全过滤级别,例如 HARM_CATEGORY_HARASSMENT: - BLOCK_NONE。 -
- - 建议设置成OFF,其他值会影响输出速度,非必要不要随便改动。 -
+ class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer" + >
- - -
-

+
+ +
- 图像生成配置 -

- - -
- - 用于图像生成的付费API密钥 -
- - -
-
- - -
- 用于图像生成的模型 -
- - -
- - - 图片上传服务提供商 -
- - -
- - - SM.MS图床的密钥 -
- - -
- - - PicGo的API密钥 -
- - -
- - - Cloudflare图床的URL -
- - -
- - - Cloudflare图床的认证码 + for="SHOW_SEARCH_LINK" + class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer" + >
- - -
-

+
+ - 流式输出优化器 -

- - -
+
+ + +
+
+ + +
+ +
+ +
+
+ + +
+ 用于"思考过程"的模型列表 +
+ + +
+ +
+ +
+ 请先在上方添加思考模型,然后在此处配置预算。 +
+
+ + + 为每个思考模型设置预算(整数,最大值 + 32767),此项与上方模型列表自动关联。 +
+ +
+ +
+ +
+ 定义模型的安全过滤阈值。 +
+
+
+ +
+ 配置模型的安全过滤级别,例如 HARM_CATEGORY_HARASSMENT: + BLOCK_NONE。 +
+ + 建议设置成OFF,其他值会影响输出速度,非必要不要随便改动。 +
+
+
+ + +
+

+ 图像生成配置 +

+ + +
+ + + 用于图像生成的付费API密钥 +
+ + +
+ +
+ + +
+ 用于图像生成的模型 +
+ + +
+ + + 图片上传服务提供商 +
+ + +
+ + + SM.MS图床的密钥 +
+ + +
+ + + PicGo的API密钥 +
+ + +
+ + + Cloudflare图床的URL +
+ + +
+ + + Cloudflare图床的认证码 +
+
+ + +
+

+ 流式输出优化器 +

+ + +
+ +
+ +
+
+ + +
+ + + 流式输出的最小延迟时间 +
+ + +
+ + + 流式输出的最大延迟时间 +
+ + +
+ + + 短文本的字符阈值 +
+ + +
+ + + 长文本的字符阈值 +
+ + +
+ + + 流式输出的分块大小 +
+ + +

+ 假流式配置 (Fake + Streaming) +

+ + +
+ +
+ + +
+
+ 当启用时,将调用非流式接口,并在等待响应期间发送空数据以维持连接。 + + +
+ + + 在启用假流式输出时,向客户端发送空数据以维持连接状态的时间间隔(建议 + 3-10 秒)。 +
+
+ + +
+

+ 定时任务配置 +

+ + +
+ + + 定时检查密钥状态的间隔时间(单位:小时) +
+ + +
+ + + 定时任务使用的时区,格式如 "Asia/Shanghai" 或 "UTC" +
+
+ + +
+

+ 日志配置 +

+ + +
+ + + 设置应用程序的日志记录详细程度 +
+ + +
+
+ 是否开启自动删除错误日志
- -
-
- - -
- - - 流式输出的最小延迟时间 -
- - -
- - - 流式输出的最大延迟时间 -
- - -
- - - 短文本的字符阈值 -
- - -
- - - 长文本的字符阈值 -
- - -
- - - 流式输出的分块大小 -
- - -

- 假流式配置 (Fake - Streaming) -

- - -
- -
- - -
-
- 当启用时,将调用非流式接口,并在等待响应期间发送空数据以维持连接。 - - -
- - - 在启用假流式输出时,向客户端发送空数据以维持连接状态的时间间隔(建议 - 3-10 秒)。 -
-
- - -
-

- 定时任务配置 -

- - -
- - - 定时检查密钥状态的间隔时间(单位:小时) -
- - -
- - - 定时任务使用的时区,格式如 "Asia/Shanghai" 或 "UTC" -
-
- - -
-

- 日志配置 -

- - -
- - - 设置应用程序的日志记录详细程度 -
- - -
-
-
- - -
+ class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer" + >
- 开启后,将自动删除指定天数前的错误日志。
- - -
+ 开启后,将自动删除指定天数前的错误日志。 +
+ + +
+ + + 选择自动删除错误日志的天数。 +
+ + +
+
是否开启自动删除请求日志 - - 选择自动删除错误日志的天数。 -
- - -
-
+ -
- - -
+ class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer" + >
- 开启后,将自动删除指定天数前的请求日志。 -
- - -
- - - 选择自动删除请求日志的天数。
+ 开启后,将自动删除指定天数前的请求日志。
- - -
- - -
- -
-
- - -
- - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - -