mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-08 09:26:36 +08:00
Merge remote-tracking branch 'origin/main' into feat/AutoRoute
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user