Merge remote-tracking branch 'origin/main' into feat/AutoRoute

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