|
|
from fastapi import FastAPI, HTTPException, Request, status
|
|
|
from fastapi.responses import JSONResponse, HTMLResponse
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
from fastapi.templating import Jinja2Templates
|
|
|
from app.models import ErrorResponse
|
|
|
from app.services import GeminiClient
|
|
|
from app.utils import (
|
|
|
APIKeyManager,
|
|
|
test_api_key,
|
|
|
format_log_message,
|
|
|
log_manager,
|
|
|
ResponseCacheManager,
|
|
|
ActiveRequestsManager,
|
|
|
clean_expired_stats,
|
|
|
update_api_call_stats,
|
|
|
check_version,
|
|
|
schedule_cache_cleanup,
|
|
|
handle_exception,
|
|
|
log
|
|
|
)
|
|
|
from app.api import router, init_router, dashboard_router, init_dashboard_router
|
|
|
from app.config.settings import (
|
|
|
FAKE_STREAMING,
|
|
|
FAKE_STREAMING_INTERVAL,
|
|
|
PASSWORD,
|
|
|
MAX_REQUESTS_PER_MINUTE,
|
|
|
MAX_REQUESTS_PER_DAY_PER_IP,
|
|
|
RETRY_DELAY,
|
|
|
MAX_RETRY_DELAY,
|
|
|
CACHE_EXPIRY_TIME,
|
|
|
MAX_CACHE_ENTRIES,
|
|
|
REMOVE_CACHE_AFTER_USE,
|
|
|
REQUEST_HISTORY_EXPIRY_TIME,
|
|
|
ENABLE_RECONNECT_DETECTION,
|
|
|
api_call_stats,
|
|
|
client_request_history,
|
|
|
local_version,
|
|
|
remote_version,
|
|
|
has_update,
|
|
|
API_KEY_DAILY_LIMIT
|
|
|
)
|
|
|
from app.config.safety import SAFETY_SETTINGS, SAFETY_SETTINGS_G2
|
|
|
import os
|
|
|
import json
|
|
|
import asyncio
|
|
|
import time
|
|
|
import logging
|
|
|
from datetime import datetime, timedelta
|
|
|
import sys
|
|
|
import pathlib
|
|
|
|
|
|
|
|
|
BASE_DIR = pathlib.Path(__file__).parent
|
|
|
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
key_manager = APIKeyManager()
|
|
|
current_api_key = key_manager.get_available_key()
|
|
|
|
|
|
|
|
|
response_cache = {}
|
|
|
|
|
|
|
|
|
response_cache_manager = ResponseCacheManager(
|
|
|
expiry_time=CACHE_EXPIRY_TIME,
|
|
|
max_entries=MAX_CACHE_ENTRIES,
|
|
|
remove_after_use=REMOVE_CACHE_AFTER_USE,
|
|
|
cache_dict=response_cache
|
|
|
)
|
|
|
|
|
|
|
|
|
active_requests_pool = {}
|
|
|
|
|
|
|
|
|
active_requests_manager = ActiveRequestsManager(requests_pool=active_requests_pool)
|
|
|
|
|
|
|
|
|
|
|
|
def switch_api_key():
|
|
|
global current_api_key
|
|
|
key = key_manager.get_available_key()
|
|
|
if key:
|
|
|
current_api_key = key
|
|
|
log('info', f"API key 替换为 → {current_api_key[:8]}...", extra={'key': current_api_key[:8], 'request_type': 'switch_key'})
|
|
|
else:
|
|
|
log('error', "API key 替换失败,所有API key都已尝试,请重新配置或稍后重试", extra={'key': 'N/A', 'request_type': 'switch_key', 'status_code': 'N/A'})
|
|
|
|
|
|
async def check_keys():
|
|
|
available_keys = []
|
|
|
for key in key_manager.api_keys:
|
|
|
is_valid = await test_api_key(key)
|
|
|
status_msg = "有效" if is_valid else "无效"
|
|
|
log('info', f"API Key {key[:10]}... {status_msg}.")
|
|
|
if is_valid:
|
|
|
available_keys.append(key)
|
|
|
if not available_keys:
|
|
|
log('error', "没有可用的 API 密钥!", extra={'key': 'N/A', 'request_type': 'startup', 'status_code': 'N/A'})
|
|
|
return available_keys
|
|
|
|
|
|
|
|
|
sys.excepthook = handle_exception
|
|
|
|
|
|
|
|
|
|
|
|
@app.on_event("startup")
|
|
|
async def startup_event():
|
|
|
log('info', "Starting Gemini API proxy...")
|
|
|
|
|
|
|
|
|
schedule_cache_cleanup(response_cache_manager, active_requests_manager)
|
|
|
|
|
|
|
|
|
await check_version()
|
|
|
|
|
|
available_keys = await check_keys()
|
|
|
if available_keys:
|
|
|
key_manager.api_keys = available_keys
|
|
|
key_manager._reset_key_stack()
|
|
|
key_manager.show_all_keys()
|
|
|
log('info', f"可用 API 密钥数量:{len(key_manager.api_keys)}")
|
|
|
log('info', f"最大重试次数设置为:{len(key_manager.api_keys)}")
|
|
|
if key_manager.api_keys:
|
|
|
all_models = await GeminiClient.list_available_models(key_manager.api_keys[0])
|
|
|
GeminiClient.AVAILABLE_MODELS = [model.replace(
|
|
|
"models/", "") for model in all_models]
|
|
|
log('info', "Available models loaded.")
|
|
|
|
|
|
|
|
|
init_router(
|
|
|
key_manager,
|
|
|
response_cache_manager,
|
|
|
active_requests_manager,
|
|
|
SAFETY_SETTINGS,
|
|
|
SAFETY_SETTINGS_G2,
|
|
|
current_api_key,
|
|
|
FAKE_STREAMING,
|
|
|
FAKE_STREAMING_INTERVAL,
|
|
|
PASSWORD,
|
|
|
MAX_REQUESTS_PER_MINUTE,
|
|
|
MAX_REQUESTS_PER_DAY_PER_IP
|
|
|
)
|
|
|
|
|
|
|
|
|
init_dashboard_router(
|
|
|
key_manager,
|
|
|
response_cache_manager,
|
|
|
active_requests_manager
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
|
async def global_exception_handler(request: Request, exc: Exception):
|
|
|
from app.utils import translate_error
|
|
|
error_message = translate_error(str(exc))
|
|
|
extra_log_unhandled_exception = {'status_code': 500, 'error_message': error_message}
|
|
|
log('error', f"Unhandled exception: {error_message}", extra=extra_log_unhandled_exception)
|
|
|
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ErrorResponse(message=str(exc), type="internal_error").dict())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.include_router(router)
|
|
|
app.include_router(dashboard_router)
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
|
async def root(request: Request):
|
|
|
|
|
|
clean_expired_stats(api_call_stats)
|
|
|
response_cache_manager.clean_expired()
|
|
|
active_requests_manager.clean_completed()
|
|
|
|
|
|
now = datetime.now()
|
|
|
|
|
|
|
|
|
last_24h_calls = sum(api_call_stats['last_24h']['total'].values())
|
|
|
|
|
|
|
|
|
one_hour_ago = now - timedelta(hours=1)
|
|
|
hourly_calls = 0
|
|
|
for hour_key, count in api_call_stats['hourly']['total'].items():
|
|
|
try:
|
|
|
hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
|
|
|
if hour_time >= one_hour_ago:
|
|
|
hourly_calls += count
|
|
|
except ValueError:
|
|
|
continue
|
|
|
|
|
|
|
|
|
one_minute_ago = now - timedelta(minutes=1)
|
|
|
minute_calls = 0
|
|
|
for minute_key, count in api_call_stats['minute']['total'].items():
|
|
|
try:
|
|
|
minute_time = datetime.strptime(minute_key, '%Y-%m-%d %H:%M')
|
|
|
if minute_time >= one_minute_ago:
|
|
|
minute_calls += count
|
|
|
except ValueError:
|
|
|
continue
|
|
|
|
|
|
|
|
|
recent_logs = log_manager.get_recent_logs(50)
|
|
|
|
|
|
|
|
|
total_cache = len(response_cache_manager.cache)
|
|
|
valid_cache = sum(1 for _, data in response_cache_manager.cache.items()
|
|
|
if time.time() < data.get('expiry_time', 0))
|
|
|
cache_by_model = {}
|
|
|
|
|
|
|
|
|
for _, cache_data in response_cache_manager.cache.items():
|
|
|
if time.time() < cache_data.get('expiry_time', 0):
|
|
|
|
|
|
model = cache_data.get('response', {}).model
|
|
|
if model:
|
|
|
if model in cache_by_model:
|
|
|
cache_by_model[model] += 1
|
|
|
else:
|
|
|
cache_by_model[model] = 1
|
|
|
|
|
|
|
|
|
history_count = len(client_request_history)
|
|
|
|
|
|
|
|
|
active_count = len(active_requests_manager.active_requests)
|
|
|
active_done = sum(1 for task in active_requests_manager.active_requests.values() if task.done())
|
|
|
active_pending = active_count - active_done
|
|
|
|
|
|
|
|
|
api_key_stats = []
|
|
|
for api_key in key_manager.api_keys:
|
|
|
|
|
|
api_key_id = api_key[:8]
|
|
|
|
|
|
|
|
|
calls_24h = 0
|
|
|
if 'by_endpoint' in api_call_stats['last_24h'] and api_key in api_call_stats['last_24h']['by_endpoint']:
|
|
|
calls_24h = sum(api_call_stats['last_24h']['by_endpoint'][api_key].values())
|
|
|
|
|
|
|
|
|
usage_percent = (calls_24h / API_KEY_DAILY_LIMIT) * 100 if API_KEY_DAILY_LIMIT > 0 else 0
|
|
|
|
|
|
|
|
|
api_key_stats.append({
|
|
|
'api_key': api_key_id,
|
|
|
'calls_24h': calls_24h,
|
|
|
'limit': API_KEY_DAILY_LIMIT,
|
|
|
'usage_percent': round(usage_percent, 2)
|
|
|
})
|
|
|
|
|
|
|
|
|
api_key_stats.sort(key=lambda x: x['usage_percent'], reverse=True)
|
|
|
|
|
|
|
|
|
context = {
|
|
|
"key_count": len(key_manager.api_keys),
|
|
|
"model_count": len(GeminiClient.AVAILABLE_MODELS),
|
|
|
"retry_count": len(key_manager.api_keys),
|
|
|
"last_24h_calls": last_24h_calls,
|
|
|
"hourly_calls": hourly_calls,
|
|
|
"minute_calls": minute_calls,
|
|
|
"max_requests_per_minute": MAX_REQUESTS_PER_MINUTE,
|
|
|
"max_requests_per_day_per_ip": MAX_REQUESTS_PER_DAY_PER_IP,
|
|
|
"current_time": datetime.now().strftime('%H:%M:%S'),
|
|
|
"logs": recent_logs,
|
|
|
|
|
|
"local_version": local_version,
|
|
|
"remote_version": remote_version,
|
|
|
"has_update": has_update,
|
|
|
|
|
|
"cache_entries": total_cache,
|
|
|
"valid_cache": valid_cache,
|
|
|
"expired_cache": total_cache - valid_cache,
|
|
|
"cache_expiry_time": CACHE_EXPIRY_TIME,
|
|
|
"max_cache_entries": MAX_CACHE_ENTRIES,
|
|
|
"cache_by_model": cache_by_model,
|
|
|
"request_history_count": history_count,
|
|
|
"enable_reconnect_detection": ENABLE_RECONNECT_DETECTION,
|
|
|
"remove_cache_after_use": REMOVE_CACHE_AFTER_USE,
|
|
|
|
|
|
"active_count": active_count,
|
|
|
"active_done": active_done,
|
|
|
"active_pending": active_pending,
|
|
|
|
|
|
"api_key_stats": api_key_stats,
|
|
|
}
|
|
|
|
|
|
|
|
|
return templates.TemplateResponse("index.html", {"request": request, **context}) |