Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ ENABLE_THINKING_BUDGET=false
DEFAULT_THINKING_BUDGET=8192

# 默认思考等级 (Thinking Level)
# Options: high, low (仅特定模型支持)
DEFAULT_THINKING_LEVEL=high
# Gemini 3 Pro 默认等级 (Options: high, low)
DEFAULT_THINKING_LEVEL_PRO=high
# Gemini 3 Flash 默认等级 (Options: high, medium, low, minimal)
DEFAULT_THINKING_LEVEL_FLASH=high

# 工具配置
# 是否默认启用 Google Search
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,8 @@ pyright_output.txt
pyright_utils_full.txt
temp_*.txt
temp_*.md
utils_errors.txt
utils_errors.txt
# React frontend build artifacts and dependencies
static/frontend/node_modules/
static/frontend/coverage/
static/frontend/dist/
6 changes: 1 addition & 5 deletions api_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
cancel_request,
chat_completions,
get_api_info,
get_css,
get_js,
get_queue_status,
health_check,
list_models,
Expand Down Expand Up @@ -49,10 +47,8 @@
__all__ = [
# 应用初始化
"create_app",
# 路由处理器
# 路由処理器
"read_index",
"get_css",
"get_js",
"get_api_info",
"health_check",
"list_models",
Expand Down
78 changes: 48 additions & 30 deletions api_utils/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import multiprocessing
import queue # <-- FIX: Added missing import for queue.Empty
import sys
import time
from asyncio import Lock, Queue
from contextlib import asynccontextmanager
from typing import Any, Awaitable, Callable
Expand Down Expand Up @@ -62,7 +63,7 @@ def _initialize_globals() -> None:
state.model_switching_lock = Lock()
state.params_cache_lock = Lock()
auth_utils.initialize_keys()
state.logger.info("API keys and global locks initialized.")
state.logger.debug("API keys and global locks initialized.")


def _initialize_proxy_settings() -> None:
Expand All @@ -79,11 +80,11 @@ def _initialize_proxy_settings() -> None:
state.PLAYWRIGHT_PROXY_SETTINGS = {"server": proxy_server_env}
if NO_PROXY_ENV:
state.PLAYWRIGHT_PROXY_SETTINGS["bypass"] = NO_PROXY_ENV.replace(",", ";")
state.logger.info(
f"Playwright proxy settings configured: {state.PLAYWRIGHT_PROXY_SETTINGS}"
state.logger.debug(
f"[代理] 已配置: {state.PLAYWRIGHT_PROXY_SETTINGS.get('server', 'N/A')}"
)
else:
state.logger.info("No proxy configured for Playwright.")
state.logger.debug("[代理] 未配置")


async def _start_stream_proxy() -> None:
Expand All @@ -96,24 +97,24 @@ async def _start_stream_proxy() -> None:
or get_environment_variable("HTTPS_PROXY")
or get_environment_variable("HTTP_PROXY")
)
state.logger.info(
f"Starting STREAM proxy on port {port} with upstream proxy: {STREAM_PROXY_SERVER_ENV}"
)
state.logger.info(f"[系统] 启动流式代理服务 (端口: {port})")
state.STREAM_QUEUE = multiprocessing.Queue()
state.STREAM_PROCESS = multiprocessing.Process(
target=stream.start,
args=(state.STREAM_QUEUE, port, STREAM_PROXY_SERVER_ENV),
)
state.STREAM_PROCESS.start()
state.logger.info("STREAM proxy process started. Waiting for 'READY' signal...")
state.logger.debug(
"STREAM proxy process started. Waiting for 'READY' signal..."
)

# Wait for the proxy to be ready
try:
# Use asyncio.to_thread to wait for the blocking queue.get()
# Set a timeout to avoid waiting forever
ready_signal = await asyncio.to_thread(state.STREAM_QUEUE.get, timeout=15)
if ready_signal == "READY":
state.logger.info("Received 'READY' signal from STREAM proxy.")
state.logger.info("[系统] 流式代理就绪")
else:
state.logger.warning(
f"Received unexpected signal from proxy: {ready_signal}"
Expand All @@ -129,10 +130,9 @@ async def _initialize_browser_and_page() -> None:
"""Initialize Playwright browser connection and page."""
from playwright.async_api import async_playwright

state.logger.info("Starting Playwright...")
state.logger.debug("[内核] 正在启动 Playwright...")
state.playwright_manager = await async_playwright().start()
state.is_playwright_ready = True
state.logger.info("Playwright started.")

ws_endpoint = get_environment_variable("CAMOUFOX_WS_ENDPOINT")
launch_mode = get_environment_variable("LAUNCH_MODE", "unknown")
Expand All @@ -141,20 +141,20 @@ async def _initialize_browser_and_page() -> None:
raise ValueError("CAMOUFOX_WS_ENDPOINT environment variable is missing.")

if ws_endpoint:
state.logger.info(f"Connecting to browser at: {ws_endpoint}")
state.logger.debug(f"Connecting to browser at: {ws_endpoint}")
state.browser_instance = await state.playwright_manager.firefox.connect(
ws_endpoint, timeout=30000
)
state.is_browser_connected = True
state.logger.info(f"Connected to browser: {state.browser_instance.version}")
state.logger.info(f"[浏览器] 已连接 (版本: {state.browser_instance.version})")

state.page_instance, state.is_page_ready = await _initialize_page_logic(
state.browser_instance
)
if state.is_page_ready:
await _handle_initial_model_state_and_storage(state.page_instance)
await enable_temporary_chat_mode(state.page_instance)
state.logger.info("Page initialized successfully.")
state.logger.info("[系统] 页面初始化成功")
else:
state.logger.error("Page initialization failed.")

Expand All @@ -165,7 +165,7 @@ async def _initialize_browser_and_page() -> None:
async def _shutdown_resources() -> None:
"""Gracefully shut down all resources."""
logger = state.logger
logger.info("Shutting down resources...")
logger.debug("[系统] 正在关闭资源...")

# Signal all streaming generators to exit immediately
state.should_exit = True
Expand All @@ -185,29 +185,29 @@ async def _shutdown_resources() -> None:
state.STREAM_QUEUE.join_thread()
except Exception:
pass
logger.info("STREAM proxy terminated.")
logger.debug("STREAM proxy terminated.")

if state.worker_task and not state.worker_task.done():
logger.info("Cancelling worker task...")
logger.debug("Cancelling worker task...")
state.worker_task.cancel()
try:
await asyncio.wait_for(state.worker_task, timeout=2.0)
logger.info("Worker task cancelled.")
logger.debug("Worker task cancelled.")
except asyncio.TimeoutError:
logger.warning("Worker task did not respond to cancellation within 2s.")
except asyncio.CancelledError:
logger.info("Worker task cancelled.")
logger.debug("Worker task cancelled.")

if state.page_instance:
await _close_page_logic()

if state.browser_instance and state.browser_instance.is_connected():
await state.browser_instance.close()
logger.info("Browser connection closed.")
logger.debug("Browser connection closed.")

if state.playwright_manager:
await state.playwright_manager.stop()
logger.info("Playwright stopped.")
logger.debug("Playwright stopped.")


@asynccontextmanager
Expand All @@ -225,7 +225,8 @@ async def lifespan(app: FastAPI):
load_excluded_models(EXCLUDED_MODELS_FILENAME)

state.is_initializing = True
logger.info("Starting AI Studio Proxy Server...")
startup_start_time = time.time()
logger.info("[系统] AI Studio 代理服务器启动中...")

try:
await _start_stream_proxy()
Expand All @@ -234,11 +235,12 @@ async def lifespan(app: FastAPI):
launch_mode = get_environment_variable("LAUNCH_MODE", "unknown")
if state.is_page_ready or launch_mode == "direct_debug_no_browser":
state.worker_task = asyncio.create_task(queue_worker())
logger.info("Request processing worker started.")
logger.debug("Request processing worker started.")
else:
raise RuntimeError("Failed to initialize browser/page, worker not started.")

logger.info("Server startup complete.")
startup_duration = time.time() - startup_start_time
logger.info(f"[系统] 服务器启动完成 (耗时: {startup_duration:.2f}秒)")
state.is_initializing = False
yield
except asyncio.CancelledError:
Expand All @@ -248,11 +250,11 @@ async def lifespan(app: FastAPI):
await _shutdown_resources()
raise RuntimeError(f"Application startup failed: {e}") from e
finally:
logger.info("Shutting down server...")
logger.info("[系统] 服务器关闭中...")
await _shutdown_resources()
restore_original_streams(initial_stdout, initial_stderr)
restore_original_streams(*original_streams)
logger.info("Server shutdown complete.")
logger.info("[系统] 服务器已关闭")


class APIKeyAuthMiddleware(BaseHTTPMiddleware):
Expand Down Expand Up @@ -330,24 +332,26 @@ def create_app() -> FastAPI:

from .routers import (
add_api_key,
auth_files_router,
cancel_request,
chat_completions,
delete_api_key,
get_api_info,
get_api_keys,
get_css,
get_js,
get_queue_status,
health_check,
list_models,
model_capabilities_router,
ports_router,
proxy_router,
read_index,
serve_react_assets,
test_api_key,
websocket_log_endpoint,
)

app.get("/", response_class=FileResponse)(read_index)
app.get("/webui.css")(get_css)
app.get("/webui.js")(get_js)
app.get("/assets/{filename:path}")(serve_react_assets) # React built assets
app.get("/api/info")(get_api_info)
app.get("/health")(health_check)
app.get("/v1/models")(list_models)
Expand All @@ -356,6 +360,20 @@ def create_app() -> FastAPI:
app.get("/v1/queue")(get_queue_status)
app.websocket("/ws/logs")(websocket_log_endpoint)

# Model capabilities endpoint (single source of truth)
app.include_router(model_capabilities_router)

# Proxy, auth, and port management routers
app.include_router(proxy_router)
app.include_router(auth_files_router)
app.include_router(ports_router)

# Server control and helper routers
from api_utils.routers import helper_router, server_router

app.include_router(server_router)
app.include_router(helper_router)

# API密钥管理端点
app.get("/api/keys")(get_api_keys)
app.post("/api/keys")(add_api_key)
Expand Down
5 changes: 3 additions & 2 deletions api_utils/context_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ async def initialize_request_context(
from api_utils.server_state import state

set_request_id(req_id)
state.logger.info("开始处理请求...")
state.logger.info(f" 请求参数 - Model: {request.model}, Stream: {request.stream}")
state.logger.debug(
f"[Request] 参数: Model={request.model}, Stream={request.stream}"
)

context: RequestContext = cast(
RequestContext,
Expand Down
11 changes: 2 additions & 9 deletions api_utils/model_switching.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ async def analyze_model_requirements(

if requested_model and requested_model != proxy_model_name:
requested_model_id = requested_model.split("/")[-1]
logger.info(f"请求使用模型: {requested_model_id}")

if parsed_model_list:
valid_model_ids = [
Expand All @@ -35,8 +34,8 @@ async def analyze_model_requirements(
context["model_id_to_use"] = requested_model_id
if current_ai_studio_model_id != requested_model_id:
context["needs_model_switching"] = True
logger.info(
f"需要切换模型: 当前={current_ai_studio_model_id} -> 目标={requested_model_id}"
logger.debug(
f"[Model] 判定需切换: {current_ai_studio_model_id} -> {requested_model_id}"
)

return context
Expand All @@ -60,17 +59,13 @@ async def handle_model_switching(

async with model_switching_lock:
if state.current_ai_studio_model_id != model_id_to_use:
logger.info(
f"准备切换模型: {state.current_ai_studio_model_id} -> {model_id_to_use}"
)
from browser_utils import switch_ai_studio_model

switch_success = await switch_ai_studio_model(page, model_id_to_use, req_id)
if switch_success:
state.current_ai_studio_model_id = model_id_to_use
context["model_actually_switched"] = True
context["current_ai_studio_model_id"] = model_id_to_use
logger.info(f"模型切换成功: {state.current_ai_studio_model_id}")
else:
# Current model ID should exist when switching fails
current_model = state.current_ai_studio_model_id or "unknown"
Expand Down Expand Up @@ -104,7 +99,6 @@ async def _handle_model_switch_failure(

async def handle_parameter_cache(req_id: str, context: RequestContext) -> None:
set_request_id(req_id)
logger = context["logger"]
params_cache_lock = context["params_cache_lock"]
page_params_cache = context["page_params_cache"]
current_ai_studio_model_id = context["current_ai_studio_model_id"]
Expand All @@ -117,7 +111,6 @@ async def handle_parameter_cache(req_id: str, context: RequestContext) -> None:
if model_actually_switched or (
current_ai_studio_model_id != cached_model_for_params
):
logger.info("模型已更改,参数缓存失效。")
page_params_cache.clear()
page_params_cache["last_known_model_id_for_params"] = (
current_ai_studio_model_id
Expand Down
Loading