Spaces:
Paused
Paused
File size: 12,740 Bytes
a583ded f3518c5 a583ded f3518c5 f8272eb f3518c5 ce9e9da 93d79eb f3518c5 4f6a1a5 f3518c5 be6825e f3518c5 be6825e f3518c5 93d79eb f3518c5 93d79eb cd70b5d ce9e9da 93d79eb a583ded f8272eb a583ded 93d79eb a583ded ff54322 cd70b5d 4f6a1a5 7f67a14 f3518c5 a583ded f8272eb a583ded f3518c5 6c09e56 f3518c5 cd70b5d 93d79eb cd70b5d 93d79eb cd70b5d 68b21f6 cd70b5d 68b21f6 cd70b5d f3518c5 93d79eb f3518c5 cd70b5d f3518c5 93d79eb 4f6a1a5 f3518c5 93d79eb 77830e0 93d79eb a583ded 93d79eb f3518c5 a583ded f8272eb a583ded f8272eb a583ded f3518c5 93d79eb a583ded 93d79eb a583ded 93d79eb f3518c5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | import ast
import os
import sys
from typing import Any, Literal, Optional
import orjson
from loguru import logger
from pydantic import BaseModel, Field, ValidationError, field_validator
from pydantic_settings import (
BaseSettings,
SettingsConfigDict,
YamlConfigSettingsSource,
)
CONFIG_PATH = "config/config.yaml"
class HTTPSConfig(BaseModel):
"""HTTPS configuration"""
enabled: bool = Field(default=False, description="Enable HTTPS")
key_file: str = Field(default="certs/privkey.pem", description="SSL private key file path")
cert_file: str = Field(default="certs/fullchain.pem", description="SSL certificate file path")
class ServerConfig(BaseModel):
"""Server configuration"""
host: str = Field(default="0.0.0.0", description="Server host address")
port: int = Field(default=8000, ge=1, le=65535, description="Server port number")
api_key: Optional[str] = Field(
default=None,
description="API key for authentication, if set, will enable API key validation",
)
https: HTTPSConfig = Field(default=HTTPSConfig(), description="HTTPS configuration")
class GeminiClientSettings(BaseModel):
"""Credential set for one Gemini client."""
id: str = Field(..., description="Unique identifier for the client")
secure_1psid: str = Field(..., description="Gemini Secure 1PSID")
secure_1psidts: str = Field(..., description="Gemini Secure 1PSIDTS")
proxy: Optional[str] = Field(default=None, description="Proxy URL for this Gemini client")
@field_validator("proxy", mode="before")
@classmethod
def _blank_proxy_to_none(cls, value: Optional[str]) -> Optional[str]:
if value is None:
return None
stripped = value.strip()
return stripped or None
class GeminiModelConfig(BaseModel):
"""Configuration for a custom Gemini model."""
model_name: Optional[str] = Field(default=None, description="Name of the model")
model_header: Optional[dict[str, Optional[str]]] = Field(
default=None, description="Header for the model"
)
@field_validator("model_header", mode="before")
@classmethod
def _parse_json_string(cls, v: Any) -> Any:
if isinstance(v, str) and v.strip().startswith("{"):
try:
return orjson.loads(v)
except orjson.JSONDecodeError:
# Return the original value to let Pydantic handle the error or type mismatch
return v
return v
class GeminiConfig(BaseModel):
"""Gemini API configuration"""
clients: list[GeminiClientSettings] = Field(
..., description="List of Gemini client credential pairs"
)
models: list[GeminiModelConfig] = Field(default=[], description="List of custom Gemini models")
model_strategy: Literal["append", "overwrite"] = Field(
default="append",
description="Strategy for loading models: 'append' merges custom with default, 'overwrite' uses only custom",
)
timeout: int = Field(default=120, ge=1, description="Init timeout")
auto_refresh: bool = Field(True, description="Enable auto-refresh for Gemini cookies")
refresh_interval: int = Field(
default=540, ge=1, description="Interval in seconds to refresh Gemini cookies"
)
verbose: bool = Field(False, description="Enable verbose logging for Gemini API requests")
max_chars_per_request: int = Field(
default=1_000_000,
ge=1,
description="Maximum characters Gemini Web can accept per request",
)
@field_validator("models", mode="before")
@classmethod
def _parse_models_json(cls, v: Any) -> Any:
if isinstance(v, str) and v.strip().startswith("["):
try:
return orjson.loads(v)
except orjson.JSONDecodeError as e:
logger.warning(f"Failed to parse models JSON string: {e}")
return v
return v
@field_validator("models")
@classmethod
def _filter_valid_models(cls, v: list[GeminiModelConfig]) -> list[GeminiModelConfig]:
"""Filter out models that don't have all required fields set."""
valid_models = []
for model in v:
if model.model_name and model.model_header:
valid_models.append(model)
else:
missing = []
if not model.model_name:
missing.append("model_name")
if not model.model_header:
missing.append("model_header")
logger.warning(
f"Discarding custom model due to missing {', '.join(missing)}: {model}"
)
return valid_models
class CORSConfig(BaseModel):
"""CORS configuration"""
enabled: bool = Field(default=True, description="Enable CORS support")
allow_origins: list[str] = Field(
default=["*"], description="List of allowed origins for CORS requests"
)
allow_credentials: bool = Field(default=True, description="Allow credentials in CORS requests")
allow_methods: list[str] = Field(
default=["*"], description="List of allowed HTTP methods for CORS requests"
)
allow_headers: list[str] = Field(
default=["*"], description="List of allowed headers for CORS requests"
)
class StorageConfig(BaseModel):
"""LMDB Storage configuration"""
path: str = Field(
default="data/lmdb",
description="Path to the storage directory where data will be saved",
)
max_size: int = Field(
default=1024**2 * 256, # 256 MB
ge=1,
description="Maximum size of the storage in bytes",
)
retention_days: int = Field(
default=14,
ge=0,
description="Number of days to retain conversations before automatic cleanup (0 disables cleanup)",
)
class LoggingConfig(BaseModel):
"""Logging configuration"""
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
default="DEBUG",
description="Logging level",
)
class Config(BaseSettings):
"""Application configuration"""
# Server configuration
server: ServerConfig = Field(
default=ServerConfig(),
description="Server configuration, including host, port, and API key",
)
# CORS configuration
cors: CORSConfig = Field(
default=CORSConfig(),
description="CORS configuration, allows cross-origin requests",
)
# Gemini API configuration
gemini: GeminiConfig = Field(..., description="Gemini API configuration, must be set")
storage: StorageConfig = Field(
default=StorageConfig(),
description="Storage configuration, defines where and how data will be stored",
)
# Logging configuration
logging: LoggingConfig = Field(
default=LoggingConfig(),
description="Logging configuration",
)
model_config = SettingsConfigDict(
env_prefix="CONFIG_",
env_nested_delimiter="__",
nested_model_default_partial_update=True,
yaml_file=os.getenv("CONFIG_PATH", CONFIG_PATH),
)
@classmethod
def settings_customise_sources(
cls,
settings_cls,
init_settings,
env_settings,
dotenv_settings,
file_secret_settings,
):
"""Read settings: env -> yaml -> default"""
return (
env_settings,
YamlConfigSettingsSource(settings_cls),
)
def extract_gemini_clients_env() -> dict[int, dict[str, str]]:
"""Extract and remove all Gemini clients related environment variables, return a mapping from index to field dict."""
prefix = "CONFIG_GEMINI__CLIENTS__"
env_overrides: dict[int, dict[str, str]] = {}
to_delete = []
for k, v in os.environ.items():
if k.startswith(prefix):
parts = k.split("__")
if len(parts) < 4:
continue
index_str, field = parts[2], parts[3].lower()
if not index_str.isdigit():
continue
idx = int(index_str)
env_overrides.setdefault(idx, {})[field] = v
to_delete.append(k)
# Remove these environment variables to avoid Pydantic parsing errors
for k in to_delete:
del os.environ[k]
return env_overrides
def _merge_clients_with_env(
base_clients: list[GeminiClientSettings] | None,
env_overrides: dict[int, dict[str, str]],
):
"""Override base_clients with env_overrides, return the new clients list."""
if not env_overrides:
return base_clients
result_clients: list[GeminiClientSettings] = []
if base_clients:
result_clients = [client.model_copy() for client in base_clients]
for idx in sorted(env_overrides):
overrides = env_overrides[idx]
if idx < len(result_clients):
client_dict = result_clients[idx].model_dump()
client_dict.update(overrides)
result_clients[idx] = GeminiClientSettings(**client_dict)
elif idx == len(result_clients):
new_client = GeminiClientSettings(**overrides)
result_clients.append(new_client)
else:
raise IndexError(
f"Client index {idx} in env is out of range (current count: {len(result_clients)}). "
"Client indices must be contiguous starting from 0."
)
return result_clients if result_clients else base_clients
def extract_gemini_models_env() -> dict[int, dict[str, Any]]:
"""Extract and remove all Gemini models related environment variables, supporting nested fields."""
root_key = "CONFIG_GEMINI__MODELS"
env_overrides: dict[int, dict[str, Any]] = {}
if root_key in os.environ:
val = os.environ[root_key]
models_list = None
parsed_successfully = False
try:
models_list = orjson.loads(val)
parsed_successfully = True
except orjson.JSONDecodeError:
try:
models_list = ast.literal_eval(val)
parsed_successfully = True
except (ValueError, SyntaxError) as e:
logger.warning(f"Failed to parse {root_key} as JSON or Python literal: {e}")
if parsed_successfully and isinstance(models_list, list):
for idx, model_data in enumerate(models_list):
if isinstance(model_data, dict):
env_overrides[idx] = model_data
# Remove the environment variable to avoid Pydantic parsing errors
del os.environ[root_key]
return env_overrides
def _merge_models_with_env(
base_models: list[GeminiModelConfig] | None,
env_overrides: dict[int, dict[str, Any]],
):
"""Override base_models with env_overrides using standard update (replace whole fields)."""
if not env_overrides:
return base_models or []
result_models: list[GeminiModelConfig] = []
if base_models:
result_models = [model.model_copy() for model in base_models]
for idx in sorted(env_overrides):
overrides = env_overrides[idx]
if idx < len(result_models):
# Update existing model: overwrite fields found in env
model_dict = result_models[idx].model_dump()
model_dict.update(overrides)
result_models[idx] = GeminiModelConfig(**model_dict)
elif idx == len(result_models):
# Append new models
new_model = GeminiModelConfig(**overrides)
result_models.append(new_model)
else:
raise IndexError(
f"Model index {idx} in env is out of range (current count: {len(result_models)}). "
"Model indices must be contiguous starting from 0."
)
return result_models
def initialize_config() -> Config:
"""
Initialize the configuration.
Returns:
Config: Configuration object
"""
try:
# First, extract and remove Gemini clients related environment variables
env_clients_overrides = extract_gemini_clients_env()
# Extract and remove Gemini models related environment variables
env_models_overrides = extract_gemini_models_env()
# Then, initialize Config with pydantic_settings
config = Config() # type: ignore
# Synthesize clients
config.gemini.clients = _merge_clients_with_env(
config.gemini.clients, env_clients_overrides
)
# Synthesize models
config.gemini.models = _merge_models_with_env(config.gemini.models, env_models_overrides)
return config
except ValidationError as e:
logger.error(f"Configuration validation failed: {e!s}")
sys.exit(1)
|