File size: 6,846 Bytes
1ebb69b
 
 
 
 
 
 
 
 
 
 
 
 
b9345ee
1ebb69b
 
 
b9345ee
 
 
1ebb69b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9345ee
1ebb69b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9345ee
 
1ebb69b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9345ee
1ebb69b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
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()