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
+15 -12
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 的起始点
@@ -429,7 +431,8 @@ async def reset_key_manager_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 提示
@@ -446,10 +449,10 @@ async def reset_key_manager_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(
"KeyManager instance has been reset. State (failure counts, old keys, next key hint) preserved for next instantiation." "KeyManager instance has been reset. State (failure counts, old keys, next key hint) preserved for next instantiation."
+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}"
) )
+5 -5
View File
@@ -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
}); });
+44 -45
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %} {% block title %}配置编辑器 - Gemini Balance{% {% extends "base.html" %} {% block title %}配置编辑器 - Gemini Balance{%
endblock %} {% block head_extra_styles %} endblock %} {% block head_extra_styles %}
<style> <style>
/* config_editor.html specific styles */ /* config_editor.html specific styles */
/* Animations (already in base.html, but keep fade-in class usage) */ /* Animations (already in base.html, but keep fade-in class usage) */
.fade-in { .fade-in {
@@ -673,9 +673,9 @@
border-color: #3b82f6 !important; /* blue-500 */ border-color: #3b82f6 !important; /* blue-500 */
box-shadow: none !important; /* 移除focus阴影 */ box-shadow: none !important; /* 移除focus阴影 */
} }
</style> </style>
{% endblock %} {% block content %} {% endblock %} {% block content %}
<div class="container max-w-6xl mx-auto px-4"> <div class="container max-w-6xl mx-auto px-4">
<div <div
class="rounded-2xl shadow-xl p-6 md:p-8" class="rounded-2xl shadow-xl p-6 md:p-8"
style=" style="
@@ -1309,7 +1309,7 @@
</div> --> </div> -->
<small class="text-gray-500 mt-1 block" <small class="text-gray-500 mt-1 block"
>为每个思考模型设置预算(整数,最大值 >为每个思考模型设置预算(整数,最大值
24576),此项与上方模型列表自动关联。</small 32767),此项与上方模型列表自动关联。</small
> >
</div> </div>
<!-- 安全设置 --> <!-- 安全设置 -->
@@ -1867,24 +1867,24 @@
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<!-- Scroll buttons are now in base.html --> <!-- Scroll buttons are now in base.html -->
<div class="scroll-buttons"> <div class="scroll-buttons">
<button class="scroll-button" onclick="scrollToTop()" title="回到顶部"> <button class="scroll-button" onclick="scrollToTop()" title="回到顶部">
<i class="fas fa-chevron-up"></i> <i class="fas fa-chevron-up"></i>
</button> </button>
<button class="scroll-button" onclick="scrollToBottom()" title="滚动到底部"> <button class="scroll-button" onclick="scrollToBottom()" title="滚动到底部">
<i class="fas fa-chevron-down"></i> <i class="fas fa-chevron-down"></i>
</button> </button>
</div> </div>
<!-- Notification component is now in base.html --> <!-- Notification component is now in base.html -->
<div id="notification" class="notification"></div> <div id="notification" class="notification"></div>
<!-- Footer is now in base.html --> <!-- Footer is now in base.html -->
<!-- API Key Add Modal --> <!-- API Key Add Modal -->
<div id="apiKeyModal" class="modal"> <div id="apiKeyModal" class="modal">
<div <div
class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in" class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
style=" style="
@@ -1930,10 +1930,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Bulk Delete API Key Modal --> <!-- Bulk Delete API Key Modal -->
<div id="bulkDeleteApiKeyModal" class="modal"> <div id="bulkDeleteApiKeyModal" class="modal">
<div <div
class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in" class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
style=" style="
@@ -1979,10 +1979,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Proxy Add Modal --> <!-- Proxy Add Modal -->
<div id="proxyModal" class="modal"> <div id="proxyModal" class="modal">
<div <div
class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in" class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
style=" style="
@@ -2028,10 +2028,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Bulk Delete Proxy Modal --> <!-- Bulk Delete Proxy Modal -->
<div id="bulkDeleteProxyModal" class="modal"> <div id="bulkDeleteProxyModal" class="modal">
<div <div
class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in" class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
style=" style="
@@ -2077,10 +2077,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Reset Confirmation Modal --> <!-- Reset Confirmation Modal -->
<div id="resetConfirmModal" class="modal"> <div id="resetConfirmModal" class="modal">
<div <div
class="w-full max-w-md mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in" class="w-full max-w-md mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
style=" style="
@@ -2120,10 +2120,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Vertex API Key Add Modal --> <!-- Vertex API Key Add Modal -->
<div id="vertexApiKeyModal" class="modal"> <div id="vertexApiKeyModal" class="modal">
<div <div
class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in" class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
style=" style="
@@ -2169,10 +2169,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Bulk Delete Vertex API Key Modal --> <!-- Bulk Delete Vertex API Key Modal -->
<div id="bulkDeleteVertexApiKeyModal" class="modal"> <div id="bulkDeleteVertexApiKeyModal" class="modal">
<div <div
class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in" class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
style=" style="
@@ -2218,10 +2218,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Model Helper Modal --> <!-- Model Helper Modal -->
<div id="modelHelperModal" class="modal"> <div id="modelHelperModal" class="modal">
<div <div
class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in" class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
style=" style="
@@ -2267,12 +2267,12 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% block body_scripts %} {% endblock %} {% block body_scripts %}
<script src="/static/js/config_editor.js"></script> <script src="/static/js/config_editor.js"></script>
<!-- 增强下拉框样式和交互性 --> <!-- 增强下拉框样式和交互性 -->
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// 增强所有下拉框的交互性 // 增强所有下拉框的交互性
const selects = document.querySelectorAll(".form-select-themed"); const selects = document.querySelectorAll(".form-select-themed");
@@ -2365,7 +2365,7 @@
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 > 24576) val = 24576; if (val > 32767) val = 32767;
} }
this.value = val; this.value = val;
@@ -2377,6 +2377,5 @@
}); });
}); });
}); });
</script> </script>
{% endblock %} {% endblock %}