Spaces:
Sleeping
Sleeping
| """Runtime configuration for the control plane. | |
| Settings are managed with ``pydantic-settings`` (``BaseSettings``), which reads | |
| from environment variables / a Hugging Face Space secret and an optional local | |
| ``.env`` file. See: | |
| https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/ | |
| The model API key is *never* required at import time: a missing key must not | |
| crash the app at startup, so it defaults to ``None`` and is only needed at the | |
| moment a live model is built. All settings share the ``OPENROUTER_`` | |
| env prefix, so the fields ``model``, ``api_key`` and ``max_turns`` map to | |
| ``OPENROUTER_MODEL``, ``OPENROUTER_API_KEY`` and ``OPENROUTER_MAX_TURNS``. | |
| """ | |
| from __future__ import annotations | |
| from pydantic import Field, field_validator | |
| from pydantic_settings import BaseSettings, SettingsConfigDict | |
| # Default model id. Overridable via OPENROUTER_MODEL with no | |
| # code changes. | |
| DEFAULT_MODEL = "openai/gpt-5-mini" | |
| # Default cap on how many turns one governed-loop run may take. See the | |
| # ``max_turns`` field below for what this protects against. | |
| DEFAULT_MAX_TURNS = 10 | |
| # Default cap on how many tool calls a live run may make in total. Each tool call | |
| # is one model round-trip (~10–15s), so this is the real bound on a live run's | |
| # wall-clock time (``max_turns`` only counts structured turns, while a model can | |
| # chain many tool calls inside a single turn). See ``tool_call_budget`` below. | |
| DEFAULT_TOOL_CALL_BUDGET = 6 | |
| class Settings(BaseSettings): | |
| """Immutable snapshot of runtime configuration. | |
| ``api_key`` may be ``None``. Constructing this never raises on a missing | |
| key; callers that actually reach the model handle its absence. | |
| """ | |
| model_config = SettingsConfigDict( | |
| env_prefix="OPENROUTER_", | |
| env_file=".env", | |
| env_file_encoding="utf-8", | |
| extra="ignore", | |
| frozen=True, | |
| ) | |
| model: str = DEFAULT_MODEL | |
| api_key: str | None = None | |
| # Hard ceiling on the number of turns a single governed-loop run may take | |
| # (one turn = propose → gate → maybe execute → feed result back). The agent | |
| # normally stops on its own by emitting a final answer; this is the safety | |
| # net for when it does not — e.g. it loops, keeps retrying a failing action, | |
| # or never concludes. Without it a live run could spin indefinitely, burning | |
| # model calls. ``ge=1`` rejects a non-positive cap (which would do nothing). | |
| max_turns: int = Field(default=DEFAULT_MAX_TURNS, ge=1) | |
| # Hard ceiling on the TOTAL number of tool calls a live run may make. This is | |
| # the real bound on a live run's wall-clock time: each tool call is a slow | |
| # model round-trip, and a model can chain many within a single turn (which | |
| # ``max_turns`` does not bound). Lower it for a snappier demo, raise it to let | |
| # the agent do more. ``ge=1`` rejects a non-positive cap. | |
| tool_call_budget: int = Field(default=DEFAULT_TOOL_CALL_BUDGET, ge=1) | |
| def _model_default_when_blank(cls, v: object) -> object: | |
| """An unset or whitespace-only model id falls back to the default.""" | |
| if v is None or (isinstance(v, str) and not v.strip()): | |
| return DEFAULT_MODEL | |
| return v | |
| def _max_turns_default_when_blank(cls, v: object) -> object: | |
| """An unset or whitespace-only value falls back to the default.""" | |
| if v is None or (isinstance(v, str) and not v.strip()): | |
| return DEFAULT_MAX_TURNS | |
| return v | |
| def _tool_budget_default_when_blank(cls, v: object) -> object: | |
| """An unset or whitespace-only value falls back to the default.""" | |
| if v is None or (isinstance(v, str) and not v.strip()): | |
| return DEFAULT_TOOL_CALL_BUDGET | |
| return v | |
| def _blank_key_is_none(cls, v: object) -> object: | |
| """Treat an empty/whitespace key as absent.""" | |
| if isinstance(v, str) and not v.strip(): | |
| return None | |
| return v | |
| def has_api_key(self) -> bool: | |
| return bool(self.api_key) | |
| def load_settings(env: dict[str, str] | None = None) -> Settings: | |
| """Load settings from the environment (and ``.env``). | |
| Parameters | |
| ---------- | |
| env: | |
| Optional mapping of ``OPENROUTER_*`` variables to read from instead of | |
| the process environment (used by tests). When provided, both fields are | |
| passed explicitly so the result is isolated from ``os.environ`` and any | |
| ``.env`` file, keeping tests deterministic. | |
| """ | |
| if env is None: | |
| return Settings() | |
| return Settings( | |
| _env_file=None, | |
| model=env.get("OPENROUTER_MODEL", DEFAULT_MODEL), | |
| api_key=env.get("OPENROUTER_API_KEY"), | |
| max_turns=env.get("OPENROUTER_MAX_TURNS", DEFAULT_MAX_TURNS), | |
| tool_call_budget=env.get("OPENROUTER_TOOL_CALL_BUDGET", DEFAULT_TOOL_CALL_BUDGET), | |
| ) | |