| | 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}) |