Add client-mode FastGPT routing

This commit is contained in:
Xin Wang
2026-07-28 08:41:34 +08:00
parent 88ba48ac77
commit 3e9385b0ca
7 changed files with 325 additions and 64 deletions
+32 -10
View File
@@ -10,21 +10,43 @@ class Config:
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "t")
# FastGPT Configuration
FASTGPT_API_KEY = os.getenv("ANALYSIS_AUTH_TOKEN")
FASTGPT_BASE_URL = os.getenv("ANALYSIS_SERVICE_URL")
FASTGPT_APP_ID = os.getenv("APP_ID")
DELETE_SESSION_URL = os.getenv("DELETE_SESSION_URL")
DELETE_CHAT_URL = os.getenv("DELETE_CHAT_URL")
GET_CHAT_RECORDS_URL = os.getenv("GET_CHAT_RECORDS_URL")
# FastGPT Configuration. Direct mode falls back to the legacy environment
# variable names so existing deployments keep working unchanged.
FASTGPT_DIRECT_API_KEY = (
os.getenv("FASTGPT_DIRECT_API_KEY")
or os.getenv("ANALYSIS_AUTH_TOKEN")
)
FASTGPT_DIRECT_BASE_URL = (
os.getenv("FASTGPT_DIRECT_BASE_URL")
or os.getenv("ANALYSIS_SERVICE_URL")
)
FASTGPT_DIRECT_APP_ID = (
os.getenv("FASTGPT_DIRECT_APP_ID")
or os.getenv("APP_ID")
)
FASTGPT_BROWSER_ADDON_API_KEY = os.getenv("FASTGPT_BROWSER_ADDON_API_KEY")
FASTGPT_BROWSER_ADDON_BASE_URL = (
os.getenv("FASTGPT_BROWSER_ADDON_BASE_URL")
or FASTGPT_DIRECT_BASE_URL
)
FASTGPT_BROWSER_ADDON_APP_ID = os.getenv("FASTGPT_BROWSER_ADDON_APP_ID")
# Backward-compatible aliases for code outside the mode-aware API layer.
FASTGPT_API_KEY = FASTGPT_DIRECT_API_KEY
FASTGPT_BASE_URL = FASTGPT_DIRECT_BASE_URL
FASTGPT_APP_ID = FASTGPT_DIRECT_APP_ID
@classmethod
def validate(cls):
"""Validate required configuration."""
required = [
("FASTGPT_API_KEY", cls.FASTGPT_API_KEY),
("FASTGPT_APP_ID", cls.FASTGPT_APP_ID),
("FASTGPT_DIRECT_BASE_URL (or ANALYSIS_SERVICE_URL)", cls.FASTGPT_DIRECT_BASE_URL),
("FASTGPT_DIRECT_API_KEY (or ANALYSIS_AUTH_TOKEN)", cls.FASTGPT_DIRECT_API_KEY),
("FASTGPT_DIRECT_APP_ID (or APP_ID)", cls.FASTGPT_DIRECT_APP_ID),
("FASTGPT_BROWSER_ADDON_API_KEY", cls.FASTGPT_BROWSER_ADDON_API_KEY),
("FASTGPT_BROWSER_ADDON_APP_ID", cls.FASTGPT_BROWSER_ADDON_APP_ID),
]
missing = [name for name, value in required if not value]
if missing:
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
+64 -27
View File
@@ -1,39 +1,76 @@
"""FastGPT client dependency injection."""
from contextlib import asynccontextmanager
from contextlib import AsyncExitStack, asynccontextmanager
from dataclasses import dataclass
from fastapi import FastAPI
from fastgpt_client import AsyncChatClient
from .config import Config
# Global client instance
_fastgpt_client: AsyncChatClient | None = None
@dataclass(frozen=True)
class FastGPTProfile:
"""FastGPT client and app metadata for one client mode."""
client: AsyncChatClient
app_id: str
_fastgpt_profiles: dict[str, FastGPTProfile] = {}
def _profile_settings() -> dict[str, dict[str, str | None]]:
return {
"direct": {
"api_key": Config.FASTGPT_DIRECT_API_KEY,
"base_url": Config.FASTGPT_DIRECT_BASE_URL,
"app_id": Config.FASTGPT_DIRECT_APP_ID,
},
"browser_addon": {
"api_key": Config.FASTGPT_BROWSER_ADDON_API_KEY,
"base_url": Config.FASTGPT_BROWSER_ADDON_BASE_URL,
"app_id": Config.FASTGPT_BROWSER_ADDON_APP_ID,
},
}
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage FastGPT client lifecycle."""
global _fastgpt_client
"""Manage all mode-specific FastGPT clients."""
Config.validate()
# Initialize client
_fastgpt_client = AsyncChatClient(
api_key=Config.FASTGPT_API_KEY,
base_url=Config.FASTGPT_BASE_URL,
timeout=60.0,
max_retries=3,
retry_delay=1.0,
enable_logging=Config.DEBUG,
)
await _fastgpt_client.__aenter__()
yield
# Cleanup
if _fastgpt_client:
await _fastgpt_client.__aexit__(None, None, None)
async with AsyncExitStack() as stack:
for client_mode, settings in _profile_settings().items():
client = AsyncChatClient(
api_key=settings["api_key"],
base_url=settings["base_url"],
timeout=60.0,
max_retries=3,
retry_delay=1.0,
enable_logging=Config.DEBUG,
)
await stack.enter_async_context(client)
_fastgpt_profiles[client_mode] = FastGPTProfile(
client=client,
app_id=settings["app_id"],
)
try:
yield
finally:
_fastgpt_profiles.clear()
def get_fastgpt_client() -> AsyncChatClient:
"""Get the FastGPT client instance."""
if _fastgpt_client is None:
raise RuntimeError("FastGPT client not initialized")
return _fastgpt_client
def get_fastgpt_profile(client_mode: str) -> FastGPTProfile:
"""Get the initialized FastGPT profile for a client mode."""
try:
return _fastgpt_profiles[client_mode]
except KeyError as exc:
if client_mode not in _profile_settings():
raise ValueError(f"Unsupported clientMode: {client_mode}") from exc
raise RuntimeError(
f"FastGPT profile is not initialized for clientMode={client_mode}"
) from exc
def get_fastgpt_client(client_mode: str = "direct") -> AsyncChatClient:
"""Get a FastGPT client, defaulting to the legacy direct mode."""
return get_fastgpt_profile(client_mode).client