Nancy / config.py
nexusagent-redis's picture
Phase 1 and 3: Production hardening and bug fixes
b9345ee
Raw
History Blame Contribute Delete
6.85 kB
"""
Nancy HF Space β€” Configuration Module.
All settings are loaded from environment variables with sensible defaults.
Provider configuration can be supplied as a JSON string via PROVIDERS_CONFIG.
"""
from __future__ import annotations
import json
import logging
from typing import Any
from dotenv import load_dotenv
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
# Load environment variables from .env file
load_dotenv()
logger = logging.getLogger("nancy.config")
class Settings(BaseSettings):
"""Application settings sourced from environment variables."""
# ── Auth ──────────────────────────────────────────────────────────
nancy_api_key: str = Field(
default="nancy-dev-key",
description="Bearer token required for /v1/* API endpoints.",
)
nancy_ext_secret: str = Field(
default="nancy-ext-dev-secret",
description="Bearer token required for /ext/* extension endpoints.",
)
# ── Upstash Redis (optional) ──────────────────────────────────────
upstash_redis_rest_url: str = Field(
default="",
description="Upstash Redis REST URL. Leave empty to use in-memory fallback.",
)
upstash_redis_rest_token: str = Field(
default="",
description="Upstash Redis REST bearer token.",
)
# ── Official Paid APIs / Hybrid Keys (optional) ───────────────────
mistral_api_key: str = Field(default="", description="Official Mistral API Key.")
nvidia_nim_api_key: str = Field(default="", description="Official NVIDIA NIM API Key.")
deepseek_api_key: str = Field(default="", description="Official DeepSeek API Key.")
anthropic_api_key: str = Field(default="", description="Official Anthropic/Claude API Key.")
z_ai_api_key: str = Field(default="", description="Official Z.ai API Key.")
# ── Provider Routing ──────────────────────────────────────────────
default_provider: str = Field(
default="chatgpt",
description="Default provider when the model name is not recognized.",
)
fallback_chain: Any = Field(
default=["chatgpt", "gemini", "deepseek", "kimi", "claude", "nim", "zai"],
description="Ordered list of providers to try on failure.",
)
providers_config: dict[str, Any] = Field(
default_factory=lambda: {
"chatgpt": {"rpm": 10, "tpm": 40000, "url_pattern": "https://chatgpt.com"},
"gemini": {"rpm": 15, "tpm": 60000, "url_pattern": "https://gemini.google.com"},
"deepseek": {"rpm": 10, "tpm": 40000, "url_pattern": "https://chat.deepseek.com"},
"kimi": {"rpm": 10, "tpm": 40000, "url_pattern": "https://kimi.moonshot.cn"},
"claude": {"rpm": 5, "tpm": 30000, "url_pattern": "https://claude.ai"},
"nim": {"rpm": 5, "tpm": 20000, "url_pattern": "https://build.nvidia.com/nim"},
"zai": {"rpm": 5, "tpm": 20000, "url_pattern": "https://chat.z.ai"},
},
description="Per-provider configuration. Supply as JSON string via env var.",
)
# ── Circuit Breaker ───────────────────────────────────────────────
cb_failure_threshold: int = Field(
default=3,
description="Consecutive failures before tripping the circuit breaker.",
)
cb_cooldown_seconds: float = Field(
default=60.0,
description="Seconds to wait before retrying a tripped provider.",
)
# ── Task Queue ────────────────────────────────────────────────────
task_timeout_seconds: float = Field(
default=240.0,
description="Max seconds to wait for extension to complete a task (fast-type + AI generation + relay).",
)
task_queue_max_size: int = Field(
default=100,
description="Maximum number of pending tasks in the queue.",
)
# ── Extension ─────────────────────────────────────────────────────
ext_heartbeat_timeout_seconds: float = Field(
default=30.0,
description="Seconds after last heartbeat before extension is considered offline.",
)
ext_sse_keepalive_seconds: float = Field(
default=15.0,
description="Interval for SSE keepalive pings to the extension.",
)
# ── Server ────────────────────────────────────────────────────────
log_level: str = Field(default="INFO", description="Logging level.")
cors_origins: Any = Field(
default=["*"],
description="Allowed CORS origins.",
)
# ── Validators ────────────────────────────────────────────────────
@field_validator("providers_config", mode="before")
@classmethod
def parse_providers_json(cls, v: Any) -> dict[str, Any]:
"""Accept a JSON string or dict for providers_config."""
if isinstance(v, str):
try:
return json.loads(v)
except json.JSONDecodeError as exc:
logger.error("Invalid PROVIDERS_CONFIG JSON: %s", exc)
raise ValueError(f"PROVIDERS_CONFIG is not valid JSON: {exc}") from exc
return v
@field_validator("fallback_chain", mode="before")
@classmethod
def parse_fallback_chain(cls, v: Any) -> list[str]:
"""Accept a comma-separated string or list."""
if isinstance(v, str):
return [s.strip() for s in v.split(",") if s.strip()]
return v
@field_validator("cors_origins", mode="before")
@classmethod
def parse_cors_origins(cls, v: Any) -> list[str]:
"""Accept a comma-separated string or list."""
if isinstance(v, str):
return [s.strip() for s in v.split(",") if s.strip()]
return v
@property
def redis_enabled(self) -> bool:
"""Return True if Upstash Redis is configured."""
return bool(self.upstash_redis_rest_url and self.upstash_redis_rest_token)
model_config = {"env_prefix": "", "case_sensitive": False}
# Module-level singleton
settings = Settings()