1 line
2.2 MiB
Plaintext
1 line
2.2 MiB
Plaintext
{"repo": "baladithyab/routeiq", "files": {"src/litellm_llmrouter/settings.py": {"6da1ff6807ecee0b3df1966b7745e9a7043979b9": {"t": 1785746923.833698, "n": 151683, "h": "2b86c1ce49260e16", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass BedrockSynthesisMode(str, Enum):\n \"\"\"How Bedrock auto-discovery folds discovered arms into ``model_list``.\n\n - ``distinct`` — one distinct ``model_name`` per discovered arm (the\n byte-stable historical default; the operator-authored\n list is only extended, never collapsed).\n - ``auto_group`` — every discovered serverless arm (across ALL providers\n and tiers) collapses onto ONE shared ``model_name``\n (``auto_group_name``) so a single routing group fans\n out over the whole Bedrock surface.\n - ``logical_groups`` — one group PER LOGICAL model (e.g.\n ``anthropic.claude-sonnet-4``) fanned across its\n global/geo/regional/mantle arms; better observability\n than ``auto_group`` (which collapses everything to one\n name) while still letting the bandit cascade per group.\n \"\"\"\n\n DISTINCT = \"distinct\"\n AUTO_GROUP = \"auto_group\"\n LOGICAL_GROUPS = \"logical_groups\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n retry_consumer_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-3ff5: enable the mode-(a) cascade RETRY consumer -- a \"\n \"component that reads the ``routeiq_cascade`` ladder on the cheapest \"\n \"rung's response, scores the response confidence, and RE-ISSUES the \"\n \"request pinned to the NEXT (stronger) rung when confidence is below \"\n \"``confidence_threshold``, climbing up to ``max_rungs`` rungs. Default \"\n \"OFF -- byte-stable: with it off a single cheapest-rung completion is \"\n \"returned unchanged. Env: \"\n \"``ROUTEIQ_COST_CASCADE__RETRY_CONSUMER_ENABLED``.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass RegionRoutingSettings(BaseModel):\n \"\"\"Per-request region-aware / data-residency candidate-filter configuration\n (RouteIQ-60cc).\n\n Bedrock cross-region inference profiles are already used STATICALLY (each\n ``model_list`` arm pins ``litellm_params.aws_region_name`` / a geo-prefixed\n inference profile). This block adds a PER-REQUEST region preference + a hard\n residency constraint on top, applied as a PRE-SCORING candidate filter (the\n same ``filter_routable_candidates`` seam every RouteIQ strategy already calls,\n so it composes with gov-ban + cooldown and benefits EVERY strategy -- no new\n strategy subclass needed).\n\n The requested region is resolved per request, FIRST match wins:\n\n 1. The request **header** named by :attr:`region_header` (read\n case-insensitively from ``request_kwargs['headers']`` /\n ``metadata['headers']``) -- e.g. ``X-RouteIQ-Region: eu``.\n 2. An explicit ``region`` / ``residency_region`` key in\n ``request_kwargs`` / ``metadata`` (e.g. a governance-workspace region the\n auth layer stamped onto the request metadata).\n\n A request is \"constrained\" (HARD residency) when EITHER the resolved value\n came with a residency flag (``residency`` / ``data_residency`` truthy in the\n same source) OR :attr:`hard_residency_default` is True. A constrained request\n is NEVER routed out-of-region: if no in-region candidate exists the candidate\n set is emptied (the strategy then returns ``None`` / triggers a fallback) --\n a residency violation must never silently leak. An UNCONSTRAINED request\n PREFERS in-region but falls back to the full set when no in-region arm exists.\n\n A candidate's region is read from ``litellm_params.aws_region_name`` (the\n field every Bedrock arm already sets), then normalized through\n :attr:`region_map` so a request token (``\"eu\"``) can map to one or more\n concrete AWS regions (``[\"eu-west-1\", \"eu-central-1\"]``). When a request\n token is absent from ``region_map`` it is matched verbatim (so an\n ``aws_region_name``-equal request token works with zero map config).\n\n Env vars (``__`` nested delimiter): ``ROUTEIQ_REGION_ROUTING__ENABLED``,\n ``ROUTEIQ_REGION_ROUTING__REGION_HEADER``,\n ``ROUTEIQ_REGION_ROUTING__REGION_MAP`` (JSON map),\n ``ROUTEIQ_REGION_ROUTING__HARD_RESIDENCY_DEFAULT``.\n\n Default OFF / identity: when ``enabled`` is False the filter is a byte-stable\n no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request region-aware / data-residency candidate \"\n \"pre-filter. Default off -> identity (byte-stable no-op).\"\n ),\n )\n region_header: str = Field(\n \"X-RouteIQ-Region\",\n description=(\n \"Request header carrying the desired region/residency token (read \"\n \"case-insensitively). Checked before the request_kwargs/metadata \"\n \"region keys.\"\n ),\n )\n region_map: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request region TOKEN -> list of concrete AWS regions that \"\n 'satisfy it (e.g. {\"eu\": [\"eu-west-1\", \"eu-central-1\"]}). A token '\n \"absent from the map is matched verbatim against the candidate \"\n \"``aws_region_name``, so an exact-region token needs no map entry.\"\n ),\n )\n hard_residency_default: bool = Field(\n False,\n description=(\n \"When True, EVERY region-constrained request is treated as a HARD \"\n \"residency constraint (never routed out-of-region) even without a \"\n \"per-request residency flag. When False (default) only requests that \"\n \"carry an explicit residency flag are hard-constrained; an \"\n \"unflagged region is a soft preference (in-region first, full-set \"\n \"fallback).\"\n ),\n )\n\n\nclass CapabilityRoutingSettings(BaseModel):\n \"\"\"INTELLIGENCE-FIRST capability-tier floor (RouteIQ-8e37).\n\n Makes the RL router pick the most CAPABLE model for the task, not the\n cheapest, by enforcing a per-request capability-tier FLOOR as a PRE-SCORING\n candidate filter on the SAME ``filter_routable_candidates`` seam every\n RouteIQ strategy already calls (the region filter — RouteIQ-60cc — is the\n precedent). It composes with gov-ban + cooldown + region and benefits EVERY\n strategy (default / ML / bandit / centroid / FALLBACK) with no new strategy\n subclass and no per-path edit.\n\n Each model carries an ordered capability TIER — ``fast`` < ``advanced`` <\n ``expert`` (see :attr:`tier_order`). The request DIFFICULTY (resolved from\n the warm centroid complexity classifier + reasoning markers — never a cold\n model load on the hot path) maps to a REQUIRED minimum tier: a simple task\n accepts any tier; a complex / reasoning task requires ``expert``. When\n gating is :attr:`enabled` and the request needs ``>=`` a tier, models BELOW\n that tier are DROPPED from the candidate set BEFORE scoring, so a reasoning\n task NEVER scores a sub-tier model — even in fallback / exploration.\n\n Tiers are CONFIG-DRIVEN: :attr:`model_tiers` overrides the per-family\n defaults (:data:`litellm_llmrouter.centroid_routing.MODEL_CAPABILITY_TIERS`).\n An UNKNOWN model defaults to :attr:`default_tier` (``advanced``, a safe\n MIDDLE tier) so it is never WRONGLY excluded by a tier floor.\n\n Capability is a PREFERENCE floor, not a hard residency deny: in the default\n SOFT mode, if the floor would EMPTY the candidate set (no capable model\n available) it DEGRADES GRACEFULLY — logs and falls back to the full set\n rather than returning None / 500. Set :attr:`strict` to fail-closed-to-empty\n instead (the strategy then yields None / triggers a fallback).\n\n Env vars (``__`` nested delimiter):\n ``ROUTEIQ_CAPABILITY_ROUTING__ENABLED``,\n ``ROUTEIQ_CAPABILITY_ROUTING__STRICT``,\n ``ROUTEIQ_CAPABILITY_ROUTING__DEFAULT_TIER``,\n ``ROUTEIQ_CAPABILITY_ROUTING__MODEL_TIERS`` (JSON map),\n ``ROUTEIQ_CAPABILITY_ROUTING__COMPLEX_MIN_TIER``,\n ``ROUTEIQ_CAPABILITY_ROUTING__SIMPLE_MIN_TIER``.\n\n Default OFF / identity: when :attr:`enabled` is False the filter is a\n byte-stable no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request capability-tier FLOOR candidate pre-filter \"\n \"(intelligence-first routing). Default off -> identity (byte-stable \"\n \"no-op).\"\n ),\n )\n strict: bool = Field(\n False,\n description=(\n \"When True, the capability floor is HARD: if no candidate meets the \"\n \"required tier the set is EMPTIED (strategy yields None / fallback). \"\n \"When False (default) the floor DEGRADES GRACEFULLY — it logs and \"\n \"returns the full set rather than emptying it (capability is a \"\n \"PREFERENCE floor, not a residency deny; never 500 on an empty set).\"\n ),\n )\n tier_order: List[str] = Field(\n default_factory=lambda: [\"fast\", \"advanced\", \"expert\"],\n description=(\n \"Ordered capability tiers from least to most capable. A model's tier \"\n \"is compared by its INDEX here; a required tier of ``expert`` drops \"\n \"every model whose tier index is below ``expert``'s. Operators may \"\n \"extend / reorder this for custom fleets.\"\n ),\n )\n default_tier: str = Field(\n \"advanced\",\n description=(\n \"Tier assigned to a model with NO known / configured tier. A SAFE \"\n \"MIDDLE tier (``advanced``) so an unknown model is never WRONGLY \"\n \"excluded by a tier floor (it clears a ``fast``/``advanced`` floor \"\n \"and is only dropped by an ``expert`` floor).\"\n ),\n )\n model_tiers: Dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Operator override map of model id (or family substring) -> tier \"\n 'name (e.g. {\"my-custom-7b\": \"fast\", \"my-frontier\": \"expert\"}). '\n \"Checked BEFORE the per-family defaults so an operator can pin any \"\n \"model's tier. A tier value not in ``tier_order`` falls back to \"\n \"``default_tier``.\"\n ),\n )\n complex_min_tier: str = Field(\n \"expert\",\n description=(\n \"Required MINIMUM tier for a COMPLEX / reasoning-difficulty request \"\n \"(centroid 'complex' tier OR reasoning markers detected). Default \"\n \"``expert`` — a hard-reasoning task never scores a sub-expert model.\"\n ),\n )\n simple_min_tier: str = Field(\n \"fast\",\n description=(\n \"Required MINIMUM tier for a SIMPLE-difficulty request. Default \"\n \"``fast`` — a simple task accepts ANY tier (the lowest floor).\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n # -- Durable-store backend selection (RouteIQ-a865) ----------------------\n # Which durable backend ``get_governance_store()`` returns. ``file``\n # (default) keeps the file-backed in-memory + Aurora path unchanged\n # (byte-stable); ``dynamodb`` selects the single-table DynamoDB store\n # (``governance_store_dynamodb.DynamoDBGovernanceStore``). The DynamoDB\n # store ALSO reads the flat ``ROUTEIQ_GOVERNANCE_BACKEND`` env directly\n # (its ``governance_backend()`` helper) so both forms agree; this field\n # surfaces the choice in the typed settings surface for discoverability.\n backend: str = Field(\n \"file\",\n description=(\n \"Durable governance store backend: ``file`` (default; file-backed \"\n \"in-memory + optional Aurora) or ``dynamodb`` (single-table \"\n \"DynamoDB). Default ``file`` -> byte-stable. Env (flat, the source \"\n \"of truth the DynamoDB store reads): ``ROUTEIQ_GOVERNANCE_BACKEND``.\"\n ),\n )\n\n\n# ============================================================================\n# Per-workspace cross-account arm synthesis (RouteIQ-c6e9) -- fresh region\n# ============================================================================\n\n\nclass WorkspaceArmSynthesisSettings(BaseModel):\n \"\"\"Per-workspace cross-account Bedrock arm synthesis (RouteIQ-c6e9).\n\n When a workspace declares an ``aws_role_arn`` (its models live in a DIFFERENT\n AWS account), RouteIQ can synthesize per-workspace Bedrock deployment arms\n (STS AssumeRole at call time) and merge them into the live ``model_list`` at\n startup. Default OFF -> byte-stable no-op (no arms synthesized, the\n operator-authored model_list is untouched).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Synthesize per-workspace cross-account Bedrock arms (from each \"\n \"registered workspace's ``aws_role_arn``) and merge them into the \"\n \"live ``model_list`` at startup. Default OFF -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS_ENABLED`` (flat) or \"\n \"``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS__ENABLED`` (nested).\"\n ),\n )\n default_region: Optional[str] = Field(\n None,\n description=(\n \"Fallback AWS region for synthesized arms when a workspace does not \"\n \"carry its own ``aws_region``. Falls back to ``AWS_REGION`` resolution \"\n \"in the synthesizer when unset. \"\n \"Env: ``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS__DEFAULT_REGION``.\"\n ),\n )\n\n\n# ============================================================================\n# AWS Secrets Manager credential vault (RouteIQ-3d33) -- fresh region\n# ============================================================================\n\n\nclass SecretsVaultSettings(BaseModel):\n \"\"\"AWS Secrets Manager credential vault for provider keys (RouteIQ-3d33).\n\n Surfaces the ``secrets_vault.SecretsVault`` backend in the typed settings\n surface. The vault resolves ``aws-secrets://<id>[#<json-key>]`` references to\n real provider keys at lookup time. Default OFF -> the vault is a transparent\n pass-through (no boto3 import, no AWS call) so a default deployment is\n byte-stable. The vault module ALSO reads the flat env vars directly so both\n forms agree.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the AWS Secrets Manager credential vault. When OFF (default) \"\n \"``resolve()`` is a transparent pass-through (no AWS call). \"\n \"Env (the source of truth the vault module reads): \"\n \"``ROUTEIQ_SECRETS_VAULT_ENABLED``.\"\n ),\n )\n region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the Secrets Manager client. Falls back to \"\n \"``AWS_REGION`` (default ``us-east-1``) in the vault when unset. \"\n \"Env: ``ROUTEIQ_SECRETS_VAULT__REGION``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n access_group_enforcement: bool = Field(\n False,\n description=(\n \"RouteIQ-2fa1: enforce per-key/per-tool MCP access groups on the LIVE \"\n \"invocation path. When True, invoking a tool whose backing server \"\n \"declares ``metadata['access_groups']`` requires the caller to present \"\n \"at least one matching access group; a non-member call is REJECTED at \"\n \"``invoke_tool`` (the terminal path, before any outbound MCP call). \"\n \"When False (default) the access-group metadata is purely advisory \"\n \"(used only by the discovery/registry listing filter) -- byte-stable. \"\n \"Servers with NO ``access_groups`` configured are unrestricted on \"\n \"either setting (open by default). Env: \"\n \"``ROUTEIQ_MCP__ACCESS_GROUP_ENFORCEMENT``.\"\n ),\n )\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n agentcore_memory_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-60e7: persist A2A agent/conversation state into AWS Bedrock \"\n \"AgentCore Memory after a successful agent invocation (via the live \"\n \"bedrock-agentcore-mcp plugin's CreateEvent). Default OFF -- \"\n \"byte-stable: the A2A invocation path is unchanged and issues no \"\n \"AgentCore call until an operator opts in. Env: \"\n \"``ROUTEIQ_A2A__AGENTCORE_MEMORY_ENABLED`` (or A2A_AGENTCORE_MEMORY_ENABLED).\"\n ),\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n # --- Circuit-breaker state-gauge re-baseline on start (RouteIQ-c714) -------\n # The ``gateway.circuit_breaker.state`` gauge is delta-driven, so a worker\n # that crashed while a breaker was OPEN leaves a STALE non-zero series until\n # the next transition / scrape staleness. When enabled, the metrics layer\n # drives every known breaker's series to a clean CLOSED baseline at startup\n # (the real boot-time state) via ``reset_circuit_breaker_state_metrics``.\n # Default OFF so the metric stream at init is byte-stable; operators relying\n # on Prometheus staleness leave it off, operators wanting a deterministic\n # boot baseline set ROUTEIQ_RESILIENCE__RESET_CIRCUIT_BREAKER_STATE_ON_START=true.\n reset_circuit_breaker_state_on_start: bool = Field(\n False,\n description=(\n \"Re-baseline the circuit-breaker state gauge to closed at startup \"\n \"(RouteIQ-c714), clearing a stale OPEN series left by a crashed \"\n \"worker. Default off (rely on Prometheus scrape staleness).\"\n ),\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass ApiSurfacesSettings(BaseModel):\n \"\"\"First-class API surfaces, persisted spend, and self-service keys\n (RouteIQ cluster N: e48a / 67fe / 3215).\n\n All flags DEFAULT to the byte-stable behaviour:\n\n * ``bare_paths_enabled`` -- register the bare-path delegators (Responses,\n rerank, RAG, batches, vector-store, audio, images) on the parent app so\n they get RouteIQ middleware + documentation (default ON; a no-op when\n LiteLLM is not mounted).\n * ``persisted_spend_enabled`` -- aggregate the spend report from the durable\n ``governance_spend`` rollup (default ON; degrades to the zero/no-op report\n automatically when no ``DATABASE_URL`` is configured).\n * ``self_service_keys_enabled`` -- expose the user-tier self-service key\n endpoints (default ON; they are scope-isolated to the caller's identity).\n\n Env vars: ``ROUTEIQ_API_SURFACES__BARE_PATHS_ENABLED``,\n ``ROUTEIQ_API_SURFACES__PERSISTED_SPEND_ENABLED``,\n ``ROUTEIQ_API_SURFACES__SELF_SERVICE_KEYS_ENABLED``.\n \"\"\"\n\n bare_paths_enabled: bool = Field(\n True,\n description=(\n \"Register first-class bare-path API surface delegators so they get \"\n \"RouteIQ middleware + OpenAPI documentation (no-op without the /v1 \"\n \"LiteLLM mount).\"\n ),\n )\n persisted_spend_enabled: bool = Field(\n True,\n description=(\n \"Aggregate the spend report from the durable governance_spend \"\n \"rollup (degrades to a zero report when no DATABASE_URL is set).\"\n ),\n )\n self_service_keys_enabled: bool = Field(\n True,\n description=(\n \"Expose user-tier self-service key endpoints (scope-isolated to the \"\n \"caller's identity).\"\n ),\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n reward_profile: str = Field(\n \"balanced\",\n description=(\n \"INTELLIGENCE-FIRST reward profile (RouteIQ-8e37). A one-flag switch \"\n \"for the bandit's reward weights:\\n\"\n \" - ``balanced`` (DEFAULT, byte-stable): the explicit \"\n \"``w_quality`` / ``w_cost`` / ``w_latency`` below (0.5 / 0.4 / 0.1).\\n\"\n \" - ``intelligence_first`` (RECOMMENDED for the Claude-Code recipe): \"\n \"weight quality ~1.0, cost ~0, latency ~0 — within the capable tier \"\n \"the bandit LEARNS per-bucket which model is actually best from the \"\n \"LLM-judge (Opus vs MiniMax vs Kimi vs DeepSeek), not the cheapest.\\n\"\n \" - ``cost_aware``: weight cost more heavily (0.2 / 0.7 / 0.1).\\n\"\n \"Any non-default profile OVERRIDES the explicit ``w_*`` weights; \"\n \"``balanced`` leaves them untouched so existing configs are unchanged. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__REWARD_PROFILE``.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n\n def resolved_reward_weights(self) -> tuple[float, float, float]:\n \"\"\"Resolve ``(w_quality, w_cost, w_latency)`` for the active profile.\n\n ``balanced`` (default) returns the explicit ``w_quality`` / ``w_cost`` /\n ``w_latency`` UNCHANGED (byte-stable). ``intelligence_first`` and\n ``cost_aware`` are one-flag presets that OVERRIDE those weights. An\n unknown profile falls back to the explicit weights (treated as balanced)\n so a typo never silently zeroes a weight.\n \"\"\"\n profile = (self.reward_profile or \"balanced\").strip().lower()\n if profile == \"intelligence_first\":\n return (1.0, 0.0, 0.0)\n if profile == \"cost_aware\":\n return (0.2, 0.7, 0.1)\n # balanced (default) or unknown -> the explicit per-weight fields.\n return (self.w_quality, self.w_cost, self.w_latency)\n\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsFailureCaptureSettings(BaseModel):\n \"\"\"Low-rate capture from the eval COLLECT FAILURE/timeout path (RouteIQ-d365).\n\n The eval COLLECT arm only samples SUCCESS responses, so AGGREGATE/FEEDBACK\n never sees which models/strategies are error-prone. When enabled, a\n parallel, clearly-labeled, low-rate capture fires on the FAILURE path\n (``async_log_failure_event``) so the loop can downweight error-prone\n models/strategies.\n\n Captured failure samples are tagged (``outcome=\"failure\"`` +\n ``error_type``) so AGGREGATE never confuses them with successful responses.\n Default OFF and sampled at a LOW rate so a failure storm cannot flood the\n eval queue.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable low-rate capture of FAILURE/timeout-path eval samples.\",\n )\n sample_rate: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Fraction of failure events to capture (kept LOW so a failure storm \"\n \"does not flood the eval queue).\"\n ),\n )\n\n\nclass MLOpsUpstreamRouterSettings(BaseModel):\n \"\"\"Delegate routing to an upstream LiteLLM adaptive/quality/auto router\n (RouteIQ-8539).\n\n Upstream LiteLLM ships its OWN online-bandit / quality-feedback routing\n modes (``litellm.router_strategy.adaptive_router`` /\n ``quality_router`` / ``auto_router``), each configured per-deployment via the\n ``auto_router/...`` model prefix and holding an in-memory\n ``AdaptiveRouterUpdateQueue``. This block lets RouteIQ DELEGATE to one of\n those modes (alongside the RouteIQ ML strategies) and FLUSH the chosen\n router's update queue to the durable store on the MLOps cadence.\n\n Default OFF: with ``enabled=False`` RouteIQ never touches the upstream\n router registries, so this is a byte-stable no-op until an operator opts in.\n The upstream router itself must additionally be configured in the LiteLLM\n ``model_list`` (the ``auto_router/...`` deployment) -- this flag only wires\n RouteIQ's delegation + queue-flush, it does not create the deployment.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Delegate to an upstream LiteLLM adaptive/quality/auto router and \"\n \"flush its update queue to the durable store.\"\n ),\n )\n mode: str = Field(\n \"adaptive\",\n description=(\n \"Which upstream router family to delegate to: 'adaptive' \"\n \"(adaptive_router), 'quality' (quality_router), or 'auto' \"\n \"(auto_router). Selects which of the LiteLLM Router's per-name \"\n \"router dicts RouteIQ delegates to / flushes.\"\n ),\n )\n router_name: str = Field(\n \"\",\n description=(\n \"Optional specific upstream router_name (the model_list deployment \"\n \"name) to delegate to. Empty => the first registered router of the \"\n \"selected mode.\"\n ),\n )\n flush_queue: bool = Field(\n True,\n description=(\n \"On each MLOps feedback cadence, drain the selected upstream \"\n \"router's AdaptiveRouterUpdateQueue (state + session) to the durable \"\n \"store (the proxy prisma client). No-op when the mode has no queue \"\n \"(auto/quality) or no durable client is available.\"\n ),\n )\n\n\nclass MLOpsSageMakerSettings(BaseModel):\n \"\"\"SageMaker Model Registry + Experiments integration (RouteIQ-93e9).\n\n Registers a trained routing-model artifact into a SageMaker Model Package\n Group (with an approval status) and logs run metrics to SageMaker\n Experiments. Cred-free + mockable: the adapter takes an injected boto3\n sagemaker client, so unit tests pass a mock and never touch AWS.\n\n Default OFF: live SageMaker calls are operator-gated. With\n ``enabled=False`` the adapter's register/log methods short-circuit and never\n construct a boto3 client.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable SageMaker Model Registry + Experiments integration. Live \"\n \"SageMaker calls are operator-gated; default off.\"\n ),\n )\n model_package_group_name: str = Field(\n \"routeiq-routing-models\",\n description=\"SageMaker Model Package Group the routing artifact registers into.\",\n )\n approval_status: str = Field(\n \"PendingManualApproval\",\n description=(\n \"ModelApprovalStatus for the registered package \"\n \"(PendingManualApproval | Approved | Rejected).\"\n ),\n )\n experiment_name: str = Field(\n \"routeiq-routing-eval\",\n description=\"SageMaker Experiment name run metrics are logged under.\",\n )\n region: str = Field(\n \"\",\n description=\"AWS region for the boto3 sagemaker client (empty => default chain).\",\n )\n\n\nclass MLOpsOfflineEvalSettings(BaseModel):\n \"\"\"Offline eval harness over a versioned golden dataset (RouteIQ-8d24).\n\n Replays a versioned golden dataset (request -> expected-quality) through the\n eval judge / a strategy and reports per-strategy quality OFFLINE -- no live\n traffic. Default OFF; the harness is also invokable directly (tests / a CLI\n / a batch job) without flipping this flag.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the offline eval harness (also directly invokable).\",\n )\n golden_dataset_path: str = Field(\n \"\",\n description=(\n \"Path to the versioned golden dataset JSON. Empty => the bundled \"\n \"default fixture under config/golden_eval/.\"\n ),\n )\n\n\nclass MLOpsRetrainingSettings(BaseModel):\n \"\"\"SageMaker scheduled-retraining orchestration (RouteIQ-8a24).\n\n Turns the manual MLflow training step into an operator-gated, scheduled loop:\n an EventBridge cron/rate rule triggers a SageMaker Training Job / Pipeline\n execution that writes the routing-model artifact to S3; on success the\n produced artifact is handed to the EXISTING SageMaker Model Registry adapter\n (``settings.mlops.sagemaker``) so train -> register -> approve -> promote\n stays one lifecycle.\n\n Cred-free + mockable: the adapter takes injected boto3 sagemaker + events\n clients, so unit tests pass mocks and never touch AWS.\n\n Default OFF: live SageMaker / EventBridge calls are operator-gated. With\n ``enabled=False`` the adapter's start/poll/register/schedule methods\n short-circuit and never construct a boto3 client.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable SageMaker scheduled-retraining orchestration. Live SageMaker \"\n \"/ EventBridge calls are operator-gated; default off.\"\n ),\n )\n mode: str = Field(\n \"training_job\",\n description=(\n \"Retraining run mode: 'training_job' (create_training_job) or \"\n \"'pipeline' (start_pipeline_execution).\"\n ),\n )\n region: str = Field(\n \"\",\n description=\"AWS region for the boto3 clients (empty => default chain).\",\n )\n training_image: str = Field(\n \"\",\n description=\"ECR image URI for the training container (training_job mode).\",\n )\n role_arn: str = Field(\n \"\",\n description=\"IAM role ARN SageMaker assumes for the retraining run.\",\n )\n instance_type: str = Field(\n \"ml.m5.xlarge\",\n description=\"Training instance type (training_job mode).\",\n )\n instance_count: int = Field(\n 1,\n description=\"Number of training instances (training_job mode).\",\n )\n volume_size_gb: int = Field(\n 30,\n description=\"Training EBS volume size in GB (training_job mode).\",\n )\n max_runtime_seconds: int = Field(\n 86400,\n description=\"Max training-job runtime in seconds before SageMaker stops it.\",\n )\n s3_artifact_bucket: str = Field(\n \"\",\n description=(\n \"S3 bucket the retraining run writes the model artifact to (the same \"\n \"location the model-artifact init/sidecar reads). Empty => no output.\"\n ),\n )\n s3_artifact_prefix: str = Field(\n \"routeiq/routing-models\",\n description=\"S3 key prefix under the artifact bucket for the model output.\",\n )\n s3_input_uri: str = Field(\n \"\",\n description=\"Optional S3 URI of the training input dataset channel.\",\n )\n pipeline_name: str = Field(\n \"routeiq-routing-retrain\",\n description=\"SageMaker Pipeline name (pipeline mode).\",\n )\n schedule_expression: str = Field(\n \"rate(7 days)\",\n description=(\n \"EventBridge cron/rate expression for the retraining schedule rule \"\n \"(e.g. 'rate(7 days)' or 'cron(0 3 ? * SUN *)').\"\n ),\n )\n schedule_rule_name: str = Field(\n \"routeiq-routing-retrain\",\n description=\"EventBridge rule name for the retraining schedule.\",\n )\n job_name_prefix: str = Field(\n \"routeiq-routing-retrain\",\n description=\"Prefix for auto-generated SageMaker training-job names.\",\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H + Cluster M).\n\n Bundles the MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), shadow/mirror canary traffic (RouteIQ-4fd1), upstream\n LiteLLM router delegation (RouteIQ-8539), SageMaker registry/experiments\n (RouteIQ-93e9), and the offline eval harness (RouteIQ-8d24). All are\n independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``,\n ``ROUTEIQ_MLOPS__UPSTREAM_ROUTER__ENABLED``,\n ``ROUTEIQ_MLOPS__SAGEMAKER__ENABLED``,\n ``ROUTEIQ_MLOPS__OFFLINE_EVAL__ENABLED``,\n ``ROUTEIQ_MLOPS__FAILURE_CAPTURE__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n upstream_router: MLOpsUpstreamRouterSettings = Field(\n default_factory=MLOpsUpstreamRouterSettings, # type: ignore[arg-type]\n description=\"Upstream LiteLLM adaptive/quality/auto router delegation.\",\n )\n sagemaker: MLOpsSageMakerSettings = Field(\n default_factory=MLOpsSageMakerSettings, # type: ignore[arg-type]\n description=\"SageMaker Model Registry + Experiments integration.\",\n )\n offline_eval: MLOpsOfflineEvalSettings = Field(\n default_factory=MLOpsOfflineEvalSettings, # type: ignore[arg-type]\n description=\"Offline eval harness over a versioned golden dataset.\",\n )\n failure_capture: MLOpsFailureCaptureSettings = Field(\n default_factory=MLOpsFailureCaptureSettings, # type: ignore[arg-type]\n description=\"Low-rate failure/timeout-path eval sample capture.\",\n )\n retraining: MLOpsRetrainingSettings = Field(\n default_factory=MLOpsRetrainingSettings, # type: ignore[arg-type]\n description=\"SageMaker scheduled-retraining orchestration (RouteIQ-8a24).\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n include_marketplace_endpoints: bool = Field(\n False,\n description=(\n \"RouteIQ-7105: also enumerate Amazon Bedrock Marketplace / mantle \"\n \"custom-deployment endpoints (``ListMarketplaceModelEndpoints``) and \"\n \"register each REGISTERED endpoint ARN as a ``model_list`` arm \"\n \"(``bedrock/<endpointArn>``). This extends the serverless-FM scan to \"\n \"cover operator-deployed Marketplace models (the bedrock-mantle \"\n \"PrivateLink custom endpoints). Default OFF -- byte-stable: the extra \"\n \"control call is opt-in and adds no arms until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__INCLUDE_MARKETPLACE_ENDPOINTS``.\"\n ),\n )\n synthesis_mode: BedrockSynthesisMode = Field(\n BedrockSynthesisMode.DISTINCT,\n description=(\n \"RouteIQ-77e8: how discovered arms fold into ``model_list``. \"\n \"``distinct`` (default, byte-stable) keeps one ``model_name`` per \"\n \"arm; ``auto_group`` collapses every arm onto the single \"\n \"``auto_group_name`` group; ``logical_groups`` emits one group per \"\n \"LOGICAL model (e.g. ``anthropic.claude-sonnet-4``) fanned across its \"\n \"global/geo/regional/mantle arms (better observability than \"\n \"``auto_group``). When set this SUPERSEDES the legacy ``auto_group`` \"\n \"bool; ``auto_group=True`` with the default ``synthesis_mode`` is \"\n \"still honoured for back-compat (it maps to ``auto_group``). Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__SYNTHESIS_MODE``.\"\n ),\n )\n full_bedrock_coverage: bool = Field(\n False,\n description=(\n \"RouteIQ-77e8: rolled-up FULL-COVERAGE switch. When True, RouteIQ \"\n \"routes across the ENTIRE Bedrock + Bedrock-Marketplace (mantle) \"\n \"surface as one operator action -- it implies ``enabled=True``, \"\n \"``include_marketplace_endpoints=True``, and \"\n \"``synthesis_mode=logical_groups`` (unless ``synthesis_mode`` was set \"\n \"explicitly to a non-default value, which wins), with the \"\n \"global>geo>regional profile preference already on by default \"\n \"(``residency_constraint=False``). So the operator flips ONE flag \"\n \"instead of four to get intelligence-first routing over every \"\n \"serverless FM + registered marketplace endpoint in every source \"\n \"region. Default OFF -- byte-stable: nothing changes until enabled. \"\n \"Env: ``ROUTEIQ_BEDROCK_DISCOVERY__FULL_BEDROCK_COVERAGE``.\"\n ),\n )\n drift_metric_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-9a42: emit a model-catalogue DRIFT metric \"\n \"(``gateway.bedrock.catalogue_drift``) measuring \"\n \"models_available (discovered) vs models_configured (the live \"\n \"model_list) so a CloudWatch alarm can fire when the deployed \"\n \"catalogue drifts from what Bedrock actually offers. Default OFF -- \"\n \"byte-stable: no metric is recorded until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_METRIC_ENABLED``.\"\n ),\n )\n drift_check_interval_seconds: float = Field(\n 900.0,\n ge=30.0,\n le=86400.0,\n description=(\n \"RouteIQ-9a42: interval (seconds) between periodic leader-gated \"\n \"catalogue-drift checks. Only consulted when \"\n \"``drift_metric_enabled`` AND ``enabled`` are True; otherwise the \"\n \"periodic task never starts (byte-stable). Default 900s (15 min). \"\n \"Env: ``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_CHECK_INTERVAL_SECONDS``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n # ------------------------------------------------------------------\n # Effective-setting resolution (RouteIQ-77e8 full-coverage rollup)\n # ------------------------------------------------------------------\n # These compute the EFFECTIVE values a scan should use after applying the\n # ``full_bedrock_coverage`` rollup, WITHOUT mutating the stored fields (so\n # the persisted config stays exactly what the operator wrote). They are the\n # single source of truth for the rollup -- both ``merge_bedrock_discovered_models``\n # and any other caller resolve through them.\n\n @property\n def effective_enabled(self) -> bool:\n \"\"\"Discovery runs when explicitly enabled OR full-coverage is on.\"\"\"\n return bool(self.enabled or self.full_bedrock_coverage)\n\n @property\n def effective_include_marketplace_endpoints(self) -> bool:\n \"\"\"Full-coverage forces marketplace/mantle endpoints into the scan.\"\"\"\n return bool(self.include_marketplace_endpoints or self.full_bedrock_coverage)\n\n @property\n def effective_synthesis_mode(self) -> \"BedrockSynthesisMode\":\n \"\"\"Resolve the synthesis mode after rollup + legacy ``auto_group`` map.\n\n Precedence (highest first):\n 1. An explicit non-default ``synthesis_mode`` (operator chose it) wins\n outright -- even under full-coverage.\n 2. ``full_bedrock_coverage`` -> ``logical_groups`` (one group per\n logical model, the recommended full-coverage shape).\n 3. Legacy ``auto_group=True`` -> ``auto_group`` (back-compat: the old\n bool still means \"collapse onto one group\" when ``synthesis_mode``\n was left at its ``distinct`` default).\n 4. Otherwise the stored ``synthesis_mode`` (``distinct`` default).\n \"\"\"\n if self.synthesis_mode is not BedrockSynthesisMode.DISTINCT:\n return self.synthesis_mode\n if self.full_bedrock_coverage:\n return BedrockSynthesisMode.LOGICAL_GROUPS\n if self.auto_group:\n return BedrockSynthesisMode.AUTO_GROUP\n return BedrockSynthesisMode.DISTINCT\n\n\nclass BedrockRequestLeversSettings(BaseModel):\n \"\"\"Per-request Bedrock Converse levers (data plane), default-OFF.\n\n Drives :class:`litellm_llmrouter.gateway.plugins.bedrock_request_levers\n .BedrockRequestLeversPlugin`, which mutates the outbound request on the\n correct litellm seam (``CustomLogger.async_pre_call_deployment_hook``) so\n the mutations actually reach the Bedrock ``Converse`` call args.\n\n Three independently-gated levers (each a byte-stable no-op when off):\n\n - ``request_metadata`` (RouteIQ-294a): forward tenant identity\n (workspace / key) as Bedrock ``requestMetadata`` tags for cost\n attribution. Lands as the top-level ``requestMetadata`` completion kwarg,\n which litellm's converse transform maps from ``non_default_params``.\n - ``team_callbacks`` (RouteIQ-9cd8): set per-team logging sinks as the\n TOP-LEVEL request kwargs ``callbacks`` / ``success_callback`` /\n ``failure_callback`` (where litellm sources dynamic per-request\n callbacks), NOT ``metadata[callbacks]`` (never read). The global\n registration is unaffected.\n - ``cache_point`` (RouteIQ-b9ee): drive Bedrock prompt-caching via\n litellm's ``cache_control_injection_points`` mechanism (system-prefix\n message injection + tool_config cachePoint), NOT a bare ``tools[]``\n ``cachePoint`` entry (dropped by the converse transform).\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_BEDROCK_LEVERS__REQUEST_METADATA``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACKS``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_POINT``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_SYSTEM``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_TOOLS``,\n ``ROUTEIQ_BEDROCK_LEVERS__METADATA_PREFIX``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACK_MAP`` (JSON\n ``{\"team-a\": {\"success_callback\": [\"s3\"]}}``).\n \"\"\"\n\n request_metadata: bool = Field(\n False,\n description=(\n \"RouteIQ-294a: forward tenant identity (workspace_id / key_id) as \"\n \"Bedrock requestMetadata tags for cost attribution. Default OFF -- \"\n \"byte-stable: no requestMetadata kwarg is added until enabled.\"\n ),\n )\n metadata_prefix: str = Field(\n \"routeiq_\",\n description=(\n \"Key prefix for the injected requestMetadata tags (Bedrock keys must \"\n \"match [a-zA-Z0-9 :_@$#=/+,.-]). Only used when request_metadata=True.\"\n ),\n )\n team_callbacks: bool = Field(\n False,\n description=(\n \"RouteIQ-9cd8: set per-team logging sinks as TOP-LEVEL request kwargs \"\n \"callbacks/success_callback/failure_callback (the seam litellm reads \"\n \"dynamic per-request callbacks from). Default OFF; the global \"\n \"registration is untouched.\"\n ),\n )\n team_callback_map: dict[str, dict[str, list[str]]] = Field(\n default_factory=dict,\n description=(\n \"Map of team_id -> {success_callback|failure_callback|callbacks: \"\n '[sink names]}. e.g. {\"team-a\": {\"success_callback\": [\"s3\"]}}. Only '\n \"applied when team_callbacks=True and the request resolves to a \"\n \"mapped team.\"\n ),\n )\n cache_point: bool = Field(\n False,\n description=(\n \"RouteIQ-b9ee: enable Bedrock prompt-caching cachePoint injection via \"\n \"litellm cache_control_injection_points. Default OFF -- byte-stable.\"\n ),\n )\n cache_system: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a system-prefix cachePoint (message \"\n \"injection point, role=system). Default on.\"\n ),\n )\n cache_tools: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a tool_config cachePoint so the tool \"\n \"schema prefix is cached. Default on. Only takes effect when the \"\n \"request carries tools.\"\n ),\n )\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\nclass EngineMetricsSettings(BaseModel):\n \"\"\"Self-hosted-engine ``/metrics`` scrape configuration (default OFF).\n\n Drives :mod:`litellm_llmrouter.engine_metrics`, the credless scraper that\n GETs a self-hosted inference engine's Prometheus ``/metrics`` (vLLM, vLLM\n Production Stack, AIBrix, llm-d -- all serve the ``vllm:*`` family) and\n parses the queue-depth / KV-cache-pressure gauges\n (``vllm:num_requests_waiting``, ``vllm:kv_cache_usage_perc`` /\n ``vllm:gpu_cache_usage_perc``, ...). Those signals feed a *future*\n KV/queue-aware Layer-1 router or an autoscaler-into-the-engine.\n\n LAYERING: this reads the engine *frontend's* aggregate ``/metrics`` so\n Layer-1 can decide whether to send more load to this engine arm; it never\n scrapes individual worker pods (that would collapse the two-layer model --\n see ``docs/architecture/aws-rearchitecture/51-multinode-large-model-serving.md``\n Part 3).\n\n Default DISABLED (opt-in): with ``enabled=False`` the scraper is a byte-stable\n no-op -- importing the module does no I/O, and ``scrape()`` returns an empty\n ``reachable=False`` snapshot without touching the network. Live scraping is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_ENGINE_METRICS__ENABLED``,\n ``ROUTEIQ_ENGINE_METRICS__SCRAPE_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the self-hosted-engine /metrics scrape. Default OFF -- the \"\n \"scraper no-ops and does zero network I/O until an operator opts in \"\n \"(live scrape is operator-gated).\"\n ),\n )\n scrape_timeout: float = Field(\n 2.0,\n ge=0.1,\n le=30.0,\n description=(\n \"Per-scrape HTTP GET timeout in seconds. An engine that does not \"\n \"answer within this budget yields an empty (reachable=False) \"\n \"snapshot, never an exception.\"\n ),\n )\n kv_aware_routing_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-08d6/6a89: enable the KV-cache-/queue-aware routing path for \"\n \"self-hosted engine arms. When ON the router scrapes each candidate \"\n \"arm's engine /metrics (vllm:num_requests_waiting, kv_cache_usage_perc) \"\n \"and selects the LEAST-LOADED arm. Requires ``enabled`` (the scraper) \"\n \"to also be True. Default OFF -- byte-stable: the KV-aware pre-step is \"\n \"skipped entirely and routing is unchanged. Env: \"\n \"``ROUTEIQ_ENGINE_METRICS__KV_AWARE_ROUTING_ENABLED``.\"\n ),\n )\n kv_cache_usage_weight: float = Field(\n 1.0,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's KV-cache usage fraction (0..1) when \"\n \"scoring engine load for KV-aware routing. Higher => avoid \"\n \"cache-pressured arms more aggressively.\"\n ),\n )\n waiting_queue_weight: float = Field(\n 0.1,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's num_requests_waiting (queue depth) when \"\n \"scoring engine load for KV-aware routing. The load score is \"\n \"kv_cache_usage_weight*kv_usage + waiting_queue_weight*num_waiting.\"\n ),\n )\n\n\nclass MultinodeAffinitySettings(BaseModel):\n \"\"\"Multinode engine-affinity passthrough configuration (default OFF).\n\n Drives the LIVE wiring of :mod:`litellm_llmrouter.engine_affinity` +\n :mod:`litellm_llmrouter.conversation_affinity` into the request path via\n :class:`~litellm_llmrouter.gateway.plugin_callback_bridge.PluginCallbackBridge`\n (RouteIQ-bdd0 + RouteIQ-3316). When enabled, the bridge's deployment hook\n derives a session/conversation affinity key from the request, looks up a\n sticky decode-worker hint, and injects ``x-worker-instance-id`` /\n ``x-routeiq-affinity-key`` headers so a multi-turn conversation lands on the\n same disaggregated decode worker; the success hook records the\n ``response_id -> deployment`` mapping for the next turn.\n\n Default DISABLED (opt-in). With ``enabled=False`` the bridge's affinity logic\n is a byte-stable no-op: no tracker lookups, no header injection, no recording.\n\n The master enable flag is the EXISTING ``ROUTEIQ_MULTINODE_AFFINITY_ENABLED``\n env var, read by :func:`engine_affinity.multinode_affinity_enabled` (the\n single source of truth for the gate); this nested model adds only the\n disaggregation-default sub-flag.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MULTINODE_AFFINITY__DISAGG_DEFAULT_REMOTE_DECODE``,\n ``ROUTEIQ_MULTINODE_AFFINITY__DISAGG_DEFAULT_REMOTE_PREFILL``.\n \"\"\"\n\n disagg_default_remote_decode: bool = Field(\n False,\n description=(\n \"RouteIQ-3316: when set, every request (under the master \"\n \"ROUTEIQ_MULTINODE_AFFINITY_ENABLED gate) carries do_remote_decode=true \"\n \"to the engine as a forward-compat disaggregation-coordination \"\n \"passthrough. RouteIQ makes NO disaggregation decision -- it only \"\n \"carries the signal. Default OFF (byte-stable: non-disagg requests add \"\n \"zero params). A per-request metadata flag overrides this default.\"\n ),\n )\n disagg_default_remote_prefill: bool = Field(\n False,\n description=(\n \"RouteIQ-3316: when set, every request (under the master \"\n \"ROUTEIQ_MULTINODE_AFFINITY_ENABLED gate) carries do_remote_prefill=true \"\n \"to the engine as a forward-compat disaggregation-coordination \"\n \"passthrough. Default OFF (byte-stable). A per-request metadata flag \"\n \"overrides this default.\"\n ),\n )\n\n\n# ============================================================================\n# Cluster TC-WIRE additions (fresh region — do not reformat blocks above)\n# ============================================================================\n\n\nclass LogArchivalSettings(BaseModel):\n \"\"\"S3 archival of full request/response logs (RouteIQ-6702).\n\n Opt-in, PII-gated long-term retention of full request/response payloads to an\n S3 bucket (compliance / audit / offline-eval corpus). Default DISABLED: when\n ``enabled=False`` the gateway registers no archival callback, builds no boto3\n client, and writes nothing to S3 (byte-stable). When enabled, a successful\n LLM call's request/response is streamed to a date-partitioned, lifecycle-tier\n -friendly S3 key the operator's S3 lifecycle policy transitions by tier.\n\n PII gate: archival of full payloads is gated behind an explicit\n ``pii_acknowledged`` flag so a deployment cannot silently begin persisting\n prompt/response text. Both ``enabled`` AND ``pii_acknowledged`` must be true\n for the live callback to be wired.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix), e.g.\n ``ROUTEIQ_LOG_ARCHIVAL__ENABLED=true``. The legacy flat\n ``ROUTEIQ_LOG_ARCHIVAL_ENABLED`` / ``ROUTEIQ_LOG_ARCHIVAL_BUCKET`` etc. read\n directly by :mod:`litellm_llmrouter.log_archival` are still honoured by the\n archiver itself; this block drives the LIVE callback wiring decision.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable S3 archival of full request/response logs. Default off \"\n \"(no callback, no boto3 client, byte-stable).\"\n ),\n )\n pii_acknowledged: bool = Field(\n False,\n description=(\n \"Explicit acknowledgement that archiving full payloads persists \"\n \"prompt/response text (PII). The live archival callback is wired only \"\n \"when this AND ``enabled`` are both true.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n # Per-workspace cross-account arm synthesis (RouteIQ-c6e9; default off).\n workspace_arm_synthesis: WorkspaceArmSynthesisSettings = Field(\n default_factory=WorkspaceArmSynthesisSettings, # type: ignore[arg-type]\n description=(\n \"Per-workspace cross-account Bedrock arm synthesis settings \"\n \"(RouteIQ-c6e9; default off / byte-stable).\"\n ),\n )\n # AWS Secrets Manager credential vault (RouteIQ-3d33; default off).\n secrets_vault: SecretsVaultSettings = Field(\n default_factory=SecretsVaultSettings, # type: ignore[arg-type]\n description=(\n \"AWS Secrets Manager credential vault settings (RouteIQ-3d33; \"\n \"default off / pass-through).\"\n ),\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n multinode_affinity: MultinodeAffinitySettings = Field(\n default_factory=MultinodeAffinitySettings, # type: ignore[arg-type]\n description=\"Multinode engine-affinity passthrough settings (RouteIQ-bdd0/3316).\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n api_surfaces: ApiSurfacesSettings = Field(\n default_factory=ApiSurfacesSettings, # type: ignore[arg-type]\n description=(\n \"First-class API surfaces, persisted spend, and self-service key \"\n \"settings (cluster N: RouteIQ-e48a / 67fe / 3215; all default on / \"\n \"byte-stable).\"\n ),\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n bedrock_levers: BedrockRequestLeversSettings = Field(\n default_factory=BedrockRequestLeversSettings, # type: ignore[arg-type]\n description=(\n \"Per-request Bedrock Converse levers (requestMetadata / team \"\n \"callbacks / cachePoint) on the pre-call mutation seam (default off).\"\n ),\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n region_routing: RegionRoutingSettings = Field(\n default_factory=RegionRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Per-request region-aware / data-residency candidate-filter settings \"\n \"(default off / identity).\"\n ),\n )\n capability_routing: CapabilityRoutingSettings = Field(\n default_factory=CapabilityRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Intelligence-first capability-tier floor candidate-filter settings \"\n \"(RouteIQ-8e37; default off / identity).\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n engine_metrics: EngineMetricsSettings = Field(\n default_factory=EngineMetricsSettings, # type: ignore[arg-type]\n description=(\n \"Self-hosted-engine /metrics scrape settings (vLLM / Production \"\n \"Stack / AIBrix / llm-d queue-depth + KV-cache gauges; default off).\"\n ),\n )\n log_archival: LogArchivalSettings = Field(\n default_factory=LogArchivalSettings, # type: ignore[arg-type]\n description=(\n \"S3 archival of full request/response logs (RouteIQ-6702; opt-in, \"\n \"PII-gated, default off / byte-stable).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_cluster_w2_aliases(cls, data: Any) -> Any:\n \"\"\"Map cluster-W2 flat env vars onto their nested settings fields.\n\n pydantic-settings won't resolve a flat top-level env var onto a nested\n ``BaseModel`` field, so (mirroring ``_map_flat_iam_aliases``) inject the\n flat values here, before validation. All DEFAULT OFF / unset, so the\n default path is byte-stable.\n\n * ``ROUTEIQ_GOVERNANCE_BACKEND`` (RouteIQ-a865) -> ``governance.backend``\n * ``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS_ENABLED`` (RouteIQ-c6e9) ->\n ``workspace_arm_synthesis.enabled``\n * ``ROUTEIQ_SECRETS_VAULT_ENABLED`` (RouteIQ-3d33) ->\n ``secrets_vault.enabled``\n\n The nested ``ROUTEIQ_<SECTION>__<FIELD>`` form WINS if both are set.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n def _set_nested(section: str, field: str, value: Any) -> None:\n current = data.get(section)\n if current is None:\n data[section] = {field: value}\n elif isinstance(current, dict):\n current.setdefault(field, value)\n elif isinstance(current, BaseModel):\n # Only fill when the field is still at its falsy default.\n if not getattr(current, field, None):\n data[section] = current.model_copy(update={field: value})\n\n backend = _os.environ.get(\"ROUTEIQ_GOVERNANCE_BACKEND\")\n if backend:\n _set_nested(\"governance\", \"backend\", backend.strip().lower())\n\n arm_enabled = _os.environ.get(\"ROUTEIQ_WORKSPACE_ARM_SYNTHESIS_ENABLED\")\n if arm_enabled is not None and _truthy(arm_enabled):\n _set_nested(\"workspace_arm_synthesis\", \"enabled\", True)\n\n vault_enabled = _os.environ.get(\"ROUTEIQ_SECRETS_VAULT_ENABLED\")\n if vault_enabled is not None and _truthy(vault_enabled):\n _set_nested(\"secrets_vault\", \"enabled\", True)\n\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "7e54a75331b759b9007cf7faef70b2cd513571cb": {"t": 1785769066.5860944, "n": 151683, "h": "2b86c1ce49260e16", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass BedrockSynthesisMode(str, Enum):\n \"\"\"How Bedrock auto-discovery folds discovered arms into ``model_list``.\n\n - ``distinct`` — one distinct ``model_name`` per discovered arm (the\n byte-stable historical default; the operator-authored\n list is only extended, never collapsed).\n - ``auto_group`` — every discovered serverless arm (across ALL providers\n and tiers) collapses onto ONE shared ``model_name``\n (``auto_group_name``) so a single routing group fans\n out over the whole Bedrock surface.\n - ``logical_groups`` — one group PER LOGICAL model (e.g.\n ``anthropic.claude-sonnet-4``) fanned across its\n global/geo/regional/mantle arms; better observability\n than ``auto_group`` (which collapses everything to one\n name) while still letting the bandit cascade per group.\n \"\"\"\n\n DISTINCT = \"distinct\"\n AUTO_GROUP = \"auto_group\"\n LOGICAL_GROUPS = \"logical_groups\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n retry_consumer_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-3ff5: enable the mode-(a) cascade RETRY consumer -- a \"\n \"component that reads the ``routeiq_cascade`` ladder on the cheapest \"\n \"rung's response, scores the response confidence, and RE-ISSUES the \"\n \"request pinned to the NEXT (stronger) rung when confidence is below \"\n \"``confidence_threshold``, climbing up to ``max_rungs`` rungs. Default \"\n \"OFF -- byte-stable: with it off a single cheapest-rung completion is \"\n \"returned unchanged. Env: \"\n \"``ROUTEIQ_COST_CASCADE__RETRY_CONSUMER_ENABLED``.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass RegionRoutingSettings(BaseModel):\n \"\"\"Per-request region-aware / data-residency candidate-filter configuration\n (RouteIQ-60cc).\n\n Bedrock cross-region inference profiles are already used STATICALLY (each\n ``model_list`` arm pins ``litellm_params.aws_region_name`` / a geo-prefixed\n inference profile). This block adds a PER-REQUEST region preference + a hard\n residency constraint on top, applied as a PRE-SCORING candidate filter (the\n same ``filter_routable_candidates`` seam every RouteIQ strategy already calls,\n so it composes with gov-ban + cooldown and benefits EVERY strategy -- no new\n strategy subclass needed).\n\n The requested region is resolved per request, FIRST match wins:\n\n 1. The request **header** named by :attr:`region_header` (read\n case-insensitively from ``request_kwargs['headers']`` /\n ``metadata['headers']``) -- e.g. ``X-RouteIQ-Region: eu``.\n 2. An explicit ``region`` / ``residency_region`` key in\n ``request_kwargs`` / ``metadata`` (e.g. a governance-workspace region the\n auth layer stamped onto the request metadata).\n\n A request is \"constrained\" (HARD residency) when EITHER the resolved value\n came with a residency flag (``residency`` / ``data_residency`` truthy in the\n same source) OR :attr:`hard_residency_default` is True. A constrained request\n is NEVER routed out-of-region: if no in-region candidate exists the candidate\n set is emptied (the strategy then returns ``None`` / triggers a fallback) --\n a residency violation must never silently leak. An UNCONSTRAINED request\n PREFERS in-region but falls back to the full set when no in-region arm exists.\n\n A candidate's region is read from ``litellm_params.aws_region_name`` (the\n field every Bedrock arm already sets), then normalized through\n :attr:`region_map` so a request token (``\"eu\"``) can map to one or more\n concrete AWS regions (``[\"eu-west-1\", \"eu-central-1\"]``). When a request\n token is absent from ``region_map`` it is matched verbatim (so an\n ``aws_region_name``-equal request token works with zero map config).\n\n Env vars (``__`` nested delimiter): ``ROUTEIQ_REGION_ROUTING__ENABLED``,\n ``ROUTEIQ_REGION_ROUTING__REGION_HEADER``,\n ``ROUTEIQ_REGION_ROUTING__REGION_MAP`` (JSON map),\n ``ROUTEIQ_REGION_ROUTING__HARD_RESIDENCY_DEFAULT``.\n\n Default OFF / identity: when ``enabled`` is False the filter is a byte-stable\n no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request region-aware / data-residency candidate \"\n \"pre-filter. Default off -> identity (byte-stable no-op).\"\n ),\n )\n region_header: str = Field(\n \"X-RouteIQ-Region\",\n description=(\n \"Request header carrying the desired region/residency token (read \"\n \"case-insensitively). Checked before the request_kwargs/metadata \"\n \"region keys.\"\n ),\n )\n region_map: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request region TOKEN -> list of concrete AWS regions that \"\n 'satisfy it (e.g. {\"eu\": [\"eu-west-1\", \"eu-central-1\"]}). A token '\n \"absent from the map is matched verbatim against the candidate \"\n \"``aws_region_name``, so an exact-region token needs no map entry.\"\n ),\n )\n hard_residency_default: bool = Field(\n False,\n description=(\n \"When True, EVERY region-constrained request is treated as a HARD \"\n \"residency constraint (never routed out-of-region) even without a \"\n \"per-request residency flag. When False (default) only requests that \"\n \"carry an explicit residency flag are hard-constrained; an \"\n \"unflagged region is a soft preference (in-region first, full-set \"\n \"fallback).\"\n ),\n )\n\n\nclass CapabilityRoutingSettings(BaseModel):\n \"\"\"INTELLIGENCE-FIRST capability-tier floor (RouteIQ-8e37).\n\n Makes the RL router pick the most CAPABLE model for the task, not the\n cheapest, by enforcing a per-request capability-tier FLOOR as a PRE-SCORING\n candidate filter on the SAME ``filter_routable_candidates`` seam every\n RouteIQ strategy already calls (the region filter — RouteIQ-60cc — is the\n precedent). It composes with gov-ban + cooldown + region and benefits EVERY\n strategy (default / ML / bandit / centroid / FALLBACK) with no new strategy\n subclass and no per-path edit.\n\n Each model carries an ordered capability TIER — ``fast`` < ``advanced`` <\n ``expert`` (see :attr:`tier_order`). The request DIFFICULTY (resolved from\n the warm centroid complexity classifier + reasoning markers — never a cold\n model load on the hot path) maps to a REQUIRED minimum tier: a simple task\n accepts any tier; a complex / reasoning task requires ``expert``. When\n gating is :attr:`enabled` and the request needs ``>=`` a tier, models BELOW\n that tier are DROPPED from the candidate set BEFORE scoring, so a reasoning\n task NEVER scores a sub-tier model — even in fallback / exploration.\n\n Tiers are CONFIG-DRIVEN: :attr:`model_tiers` overrides the per-family\n defaults (:data:`litellm_llmrouter.centroid_routing.MODEL_CAPABILITY_TIERS`).\n An UNKNOWN model defaults to :attr:`default_tier` (``advanced``, a safe\n MIDDLE tier) so it is never WRONGLY excluded by a tier floor.\n\n Capability is a PREFERENCE floor, not a hard residency deny: in the default\n SOFT mode, if the floor would EMPTY the candidate set (no capable model\n available) it DEGRADES GRACEFULLY — logs and falls back to the full set\n rather than returning None / 500. Set :attr:`strict` to fail-closed-to-empty\n instead (the strategy then yields None / triggers a fallback).\n\n Env vars (``__`` nested delimiter):\n ``ROUTEIQ_CAPABILITY_ROUTING__ENABLED``,\n ``ROUTEIQ_CAPABILITY_ROUTING__STRICT``,\n ``ROUTEIQ_CAPABILITY_ROUTING__DEFAULT_TIER``,\n ``ROUTEIQ_CAPABILITY_ROUTING__MODEL_TIERS`` (JSON map),\n ``ROUTEIQ_CAPABILITY_ROUTING__COMPLEX_MIN_TIER``,\n ``ROUTEIQ_CAPABILITY_ROUTING__SIMPLE_MIN_TIER``.\n\n Default OFF / identity: when :attr:`enabled` is False the filter is a\n byte-stable no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request capability-tier FLOOR candidate pre-filter \"\n \"(intelligence-first routing). Default off -> identity (byte-stable \"\n \"no-op).\"\n ),\n )\n strict: bool = Field(\n False,\n description=(\n \"When True, the capability floor is HARD: if no candidate meets the \"\n \"required tier the set is EMPTIED (strategy yields None / fallback). \"\n \"When False (default) the floor DEGRADES GRACEFULLY — it logs and \"\n \"returns the full set rather than emptying it (capability is a \"\n \"PREFERENCE floor, not a residency deny; never 500 on an empty set).\"\n ),\n )\n tier_order: List[str] = Field(\n default_factory=lambda: [\"fast\", \"advanced\", \"expert\"],\n description=(\n \"Ordered capability tiers from least to most capable. A model's tier \"\n \"is compared by its INDEX here; a required tier of ``expert`` drops \"\n \"every model whose tier index is below ``expert``'s. Operators may \"\n \"extend / reorder this for custom fleets.\"\n ),\n )\n default_tier: str = Field(\n \"advanced\",\n description=(\n \"Tier assigned to a model with NO known / configured tier. A SAFE \"\n \"MIDDLE tier (``advanced``) so an unknown model is never WRONGLY \"\n \"excluded by a tier floor (it clears a ``fast``/``advanced`` floor \"\n \"and is only dropped by an ``expert`` floor).\"\n ),\n )\n model_tiers: Dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Operator override map of model id (or family substring) -> tier \"\n 'name (e.g. {\"my-custom-7b\": \"fast\", \"my-frontier\": \"expert\"}). '\n \"Checked BEFORE the per-family defaults so an operator can pin any \"\n \"model's tier. A tier value not in ``tier_order`` falls back to \"\n \"``default_tier``.\"\n ),\n )\n complex_min_tier: str = Field(\n \"expert\",\n description=(\n \"Required MINIMUM tier for a COMPLEX / reasoning-difficulty request \"\n \"(centroid 'complex' tier OR reasoning markers detected). Default \"\n \"``expert`` — a hard-reasoning task never scores a sub-expert model.\"\n ),\n )\n simple_min_tier: str = Field(\n \"fast\",\n description=(\n \"Required MINIMUM tier for a SIMPLE-difficulty request. Default \"\n \"``fast`` — a simple task accepts ANY tier (the lowest floor).\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n # -- Durable-store backend selection (RouteIQ-a865) ----------------------\n # Which durable backend ``get_governance_store()`` returns. ``file``\n # (default) keeps the file-backed in-memory + Aurora path unchanged\n # (byte-stable); ``dynamodb`` selects the single-table DynamoDB store\n # (``governance_store_dynamodb.DynamoDBGovernanceStore``). The DynamoDB\n # store ALSO reads the flat ``ROUTEIQ_GOVERNANCE_BACKEND`` env directly\n # (its ``governance_backend()`` helper) so both forms agree; this field\n # surfaces the choice in the typed settings surface for discoverability.\n backend: str = Field(\n \"file\",\n description=(\n \"Durable governance store backend: ``file`` (default; file-backed \"\n \"in-memory + optional Aurora) or ``dynamodb`` (single-table \"\n \"DynamoDB). Default ``file`` -> byte-stable. Env (flat, the source \"\n \"of truth the DynamoDB store reads): ``ROUTEIQ_GOVERNANCE_BACKEND``.\"\n ),\n )\n\n\n# ============================================================================\n# Per-workspace cross-account arm synthesis (RouteIQ-c6e9) -- fresh region\n# ============================================================================\n\n\nclass WorkspaceArmSynthesisSettings(BaseModel):\n \"\"\"Per-workspace cross-account Bedrock arm synthesis (RouteIQ-c6e9).\n\n When a workspace declares an ``aws_role_arn`` (its models live in a DIFFERENT\n AWS account), RouteIQ can synthesize per-workspace Bedrock deployment arms\n (STS AssumeRole at call time) and merge them into the live ``model_list`` at\n startup. Default OFF -> byte-stable no-op (no arms synthesized, the\n operator-authored model_list is untouched).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Synthesize per-workspace cross-account Bedrock arms (from each \"\n \"registered workspace's ``aws_role_arn``) and merge them into the \"\n \"live ``model_list`` at startup. Default OFF -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS_ENABLED`` (flat) or \"\n \"``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS__ENABLED`` (nested).\"\n ),\n )\n default_region: Optional[str] = Field(\n None,\n description=(\n \"Fallback AWS region for synthesized arms when a workspace does not \"\n \"carry its own ``aws_region``. Falls back to ``AWS_REGION`` resolution \"\n \"in the synthesizer when unset. \"\n \"Env: ``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS__DEFAULT_REGION``.\"\n ),\n )\n\n\n# ============================================================================\n# AWS Secrets Manager credential vault (RouteIQ-3d33) -- fresh region\n# ============================================================================\n\n\nclass SecretsVaultSettings(BaseModel):\n \"\"\"AWS Secrets Manager credential vault for provider keys (RouteIQ-3d33).\n\n Surfaces the ``secrets_vault.SecretsVault`` backend in the typed settings\n surface. The vault resolves ``aws-secrets://<id>[#<json-key>]`` references to\n real provider keys at lookup time. Default OFF -> the vault is a transparent\n pass-through (no boto3 import, no AWS call) so a default deployment is\n byte-stable. The vault module ALSO reads the flat env vars directly so both\n forms agree.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the AWS Secrets Manager credential vault. When OFF (default) \"\n \"``resolve()`` is a transparent pass-through (no AWS call). \"\n \"Env (the source of truth the vault module reads): \"\n \"``ROUTEIQ_SECRETS_VAULT_ENABLED``.\"\n ),\n )\n region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the Secrets Manager client. Falls back to \"\n \"``AWS_REGION`` (default ``us-east-1``) in the vault when unset. \"\n \"Env: ``ROUTEIQ_SECRETS_VAULT__REGION``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n access_group_enforcement: bool = Field(\n False,\n description=(\n \"RouteIQ-2fa1: enforce per-key/per-tool MCP access groups on the LIVE \"\n \"invocation path. When True, invoking a tool whose backing server \"\n \"declares ``metadata['access_groups']`` requires the caller to present \"\n \"at least one matching access group; a non-member call is REJECTED at \"\n \"``invoke_tool`` (the terminal path, before any outbound MCP call). \"\n \"When False (default) the access-group metadata is purely advisory \"\n \"(used only by the discovery/registry listing filter) -- byte-stable. \"\n \"Servers with NO ``access_groups`` configured are unrestricted on \"\n \"either setting (open by default). Env: \"\n \"``ROUTEIQ_MCP__ACCESS_GROUP_ENFORCEMENT``.\"\n ),\n )\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n agentcore_memory_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-60e7: persist A2A agent/conversation state into AWS Bedrock \"\n \"AgentCore Memory after a successful agent invocation (via the live \"\n \"bedrock-agentcore-mcp plugin's CreateEvent). Default OFF -- \"\n \"byte-stable: the A2A invocation path is unchanged and issues no \"\n \"AgentCore call until an operator opts in. Env: \"\n \"``ROUTEIQ_A2A__AGENTCORE_MEMORY_ENABLED`` (or A2A_AGENTCORE_MEMORY_ENABLED).\"\n ),\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n # --- Circuit-breaker state-gauge re-baseline on start (RouteIQ-c714) -------\n # The ``gateway.circuit_breaker.state`` gauge is delta-driven, so a worker\n # that crashed while a breaker was OPEN leaves a STALE non-zero series until\n # the next transition / scrape staleness. When enabled, the metrics layer\n # drives every known breaker's series to a clean CLOSED baseline at startup\n # (the real boot-time state) via ``reset_circuit_breaker_state_metrics``.\n # Default OFF so the metric stream at init is byte-stable; operators relying\n # on Prometheus staleness leave it off, operators wanting a deterministic\n # boot baseline set ROUTEIQ_RESILIENCE__RESET_CIRCUIT_BREAKER_STATE_ON_START=true.\n reset_circuit_breaker_state_on_start: bool = Field(\n False,\n description=(\n \"Re-baseline the circuit-breaker state gauge to closed at startup \"\n \"(RouteIQ-c714), clearing a stale OPEN series left by a crashed \"\n \"worker. Default off (rely on Prometheus scrape staleness).\"\n ),\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass ApiSurfacesSettings(BaseModel):\n \"\"\"First-class API surfaces, persisted spend, and self-service keys\n (RouteIQ cluster N: e48a / 67fe / 3215).\n\n All flags DEFAULT to the byte-stable behaviour:\n\n * ``bare_paths_enabled`` -- register the bare-path delegators (Responses,\n rerank, RAG, batches, vector-store, audio, images) on the parent app so\n they get RouteIQ middleware + documentation (default ON; a no-op when\n LiteLLM is not mounted).\n * ``persisted_spend_enabled`` -- aggregate the spend report from the durable\n ``governance_spend`` rollup (default ON; degrades to the zero/no-op report\n automatically when no ``DATABASE_URL`` is configured).\n * ``self_service_keys_enabled`` -- expose the user-tier self-service key\n endpoints (default ON; they are scope-isolated to the caller's identity).\n\n Env vars: ``ROUTEIQ_API_SURFACES__BARE_PATHS_ENABLED``,\n ``ROUTEIQ_API_SURFACES__PERSISTED_SPEND_ENABLED``,\n ``ROUTEIQ_API_SURFACES__SELF_SERVICE_KEYS_ENABLED``.\n \"\"\"\n\n bare_paths_enabled: bool = Field(\n True,\n description=(\n \"Register first-class bare-path API surface delegators so they get \"\n \"RouteIQ middleware + OpenAPI documentation (no-op without the /v1 \"\n \"LiteLLM mount).\"\n ),\n )\n persisted_spend_enabled: bool = Field(\n True,\n description=(\n \"Aggregate the spend report from the durable governance_spend \"\n \"rollup (degrades to a zero report when no DATABASE_URL is set).\"\n ),\n )\n self_service_keys_enabled: bool = Field(\n True,\n description=(\n \"Expose user-tier self-service key endpoints (scope-isolated to the \"\n \"caller's identity).\"\n ),\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n reward_profile: str = Field(\n \"balanced\",\n description=(\n \"INTELLIGENCE-FIRST reward profile (RouteIQ-8e37). A one-flag switch \"\n \"for the bandit's reward weights:\\n\"\n \" - ``balanced`` (DEFAULT, byte-stable): the explicit \"\n \"``w_quality`` / ``w_cost`` / ``w_latency`` below (0.5 / 0.4 / 0.1).\\n\"\n \" - ``intelligence_first`` (RECOMMENDED for the Claude-Code recipe): \"\n \"weight quality ~1.0, cost ~0, latency ~0 — within the capable tier \"\n \"the bandit LEARNS per-bucket which model is actually best from the \"\n \"LLM-judge (Opus vs MiniMax vs Kimi vs DeepSeek), not the cheapest.\\n\"\n \" - ``cost_aware``: weight cost more heavily (0.2 / 0.7 / 0.1).\\n\"\n \"Any non-default profile OVERRIDES the explicit ``w_*`` weights; \"\n \"``balanced`` leaves them untouched so existing configs are unchanged. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__REWARD_PROFILE``.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n\n def resolved_reward_weights(self) -> tuple[float, float, float]:\n \"\"\"Resolve ``(w_quality, w_cost, w_latency)`` for the active profile.\n\n ``balanced`` (default) returns the explicit ``w_quality`` / ``w_cost`` /\n ``w_latency`` UNCHANGED (byte-stable). ``intelligence_first`` and\n ``cost_aware`` are one-flag presets that OVERRIDE those weights. An\n unknown profile falls back to the explicit weights (treated as balanced)\n so a typo never silently zeroes a weight.\n \"\"\"\n profile = (self.reward_profile or \"balanced\").strip().lower()\n if profile == \"intelligence_first\":\n return (1.0, 0.0, 0.0)\n if profile == \"cost_aware\":\n return (0.2, 0.7, 0.1)\n # balanced (default) or unknown -> the explicit per-weight fields.\n return (self.w_quality, self.w_cost, self.w_latency)\n\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsFailureCaptureSettings(BaseModel):\n \"\"\"Low-rate capture from the eval COLLECT FAILURE/timeout path (RouteIQ-d365).\n\n The eval COLLECT arm only samples SUCCESS responses, so AGGREGATE/FEEDBACK\n never sees which models/strategies are error-prone. When enabled, a\n parallel, clearly-labeled, low-rate capture fires on the FAILURE path\n (``async_log_failure_event``) so the loop can downweight error-prone\n models/strategies.\n\n Captured failure samples are tagged (``outcome=\"failure\"`` +\n ``error_type``) so AGGREGATE never confuses them with successful responses.\n Default OFF and sampled at a LOW rate so a failure storm cannot flood the\n eval queue.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable low-rate capture of FAILURE/timeout-path eval samples.\",\n )\n sample_rate: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Fraction of failure events to capture (kept LOW so a failure storm \"\n \"does not flood the eval queue).\"\n ),\n )\n\n\nclass MLOpsUpstreamRouterSettings(BaseModel):\n \"\"\"Delegate routing to an upstream LiteLLM adaptive/quality/auto router\n (RouteIQ-8539).\n\n Upstream LiteLLM ships its OWN online-bandit / quality-feedback routing\n modes (``litellm.router_strategy.adaptive_router`` /\n ``quality_router`` / ``auto_router``), each configured per-deployment via the\n ``auto_router/...`` model prefix and holding an in-memory\n ``AdaptiveRouterUpdateQueue``. This block lets RouteIQ DELEGATE to one of\n those modes (alongside the RouteIQ ML strategies) and FLUSH the chosen\n router's update queue to the durable store on the MLOps cadence.\n\n Default OFF: with ``enabled=False`` RouteIQ never touches the upstream\n router registries, so this is a byte-stable no-op until an operator opts in.\n The upstream router itself must additionally be configured in the LiteLLM\n ``model_list`` (the ``auto_router/...`` deployment) -- this flag only wires\n RouteIQ's delegation + queue-flush, it does not create the deployment.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Delegate to an upstream LiteLLM adaptive/quality/auto router and \"\n \"flush its update queue to the durable store.\"\n ),\n )\n mode: str = Field(\n \"adaptive\",\n description=(\n \"Which upstream router family to delegate to: 'adaptive' \"\n \"(adaptive_router), 'quality' (quality_router), or 'auto' \"\n \"(auto_router). Selects which of the LiteLLM Router's per-name \"\n \"router dicts RouteIQ delegates to / flushes.\"\n ),\n )\n router_name: str = Field(\n \"\",\n description=(\n \"Optional specific upstream router_name (the model_list deployment \"\n \"name) to delegate to. Empty => the first registered router of the \"\n \"selected mode.\"\n ),\n )\n flush_queue: bool = Field(\n True,\n description=(\n \"On each MLOps feedback cadence, drain the selected upstream \"\n \"router's AdaptiveRouterUpdateQueue (state + session) to the durable \"\n \"store (the proxy prisma client). No-op when the mode has no queue \"\n \"(auto/quality) or no durable client is available.\"\n ),\n )\n\n\nclass MLOpsSageMakerSettings(BaseModel):\n \"\"\"SageMaker Model Registry + Experiments integration (RouteIQ-93e9).\n\n Registers a trained routing-model artifact into a SageMaker Model Package\n Group (with an approval status) and logs run metrics to SageMaker\n Experiments. Cred-free + mockable: the adapter takes an injected boto3\n sagemaker client, so unit tests pass a mock and never touch AWS.\n\n Default OFF: live SageMaker calls are operator-gated. With\n ``enabled=False`` the adapter's register/log methods short-circuit and never\n construct a boto3 client.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable SageMaker Model Registry + Experiments integration. Live \"\n \"SageMaker calls are operator-gated; default off.\"\n ),\n )\n model_package_group_name: str = Field(\n \"routeiq-routing-models\",\n description=\"SageMaker Model Package Group the routing artifact registers into.\",\n )\n approval_status: str = Field(\n \"PendingManualApproval\",\n description=(\n \"ModelApprovalStatus for the registered package \"\n \"(PendingManualApproval | Approved | Rejected).\"\n ),\n )\n experiment_name: str = Field(\n \"routeiq-routing-eval\",\n description=\"SageMaker Experiment name run metrics are logged under.\",\n )\n region: str = Field(\n \"\",\n description=\"AWS region for the boto3 sagemaker client (empty => default chain).\",\n )\n\n\nclass MLOpsOfflineEvalSettings(BaseModel):\n \"\"\"Offline eval harness over a versioned golden dataset (RouteIQ-8d24).\n\n Replays a versioned golden dataset (request -> expected-quality) through the\n eval judge / a strategy and reports per-strategy quality OFFLINE -- no live\n traffic. Default OFF; the harness is also invokable directly (tests / a CLI\n / a batch job) without flipping this flag.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the offline eval harness (also directly invokable).\",\n )\n golden_dataset_path: str = Field(\n \"\",\n description=(\n \"Path to the versioned golden dataset JSON. Empty => the bundled \"\n \"default fixture under config/golden_eval/.\"\n ),\n )\n\n\nclass MLOpsRetrainingSettings(BaseModel):\n \"\"\"SageMaker scheduled-retraining orchestration (RouteIQ-8a24).\n\n Turns the manual MLflow training step into an operator-gated, scheduled loop:\n an EventBridge cron/rate rule triggers a SageMaker Training Job / Pipeline\n execution that writes the routing-model artifact to S3; on success the\n produced artifact is handed to the EXISTING SageMaker Model Registry adapter\n (``settings.mlops.sagemaker``) so train -> register -> approve -> promote\n stays one lifecycle.\n\n Cred-free + mockable: the adapter takes injected boto3 sagemaker + events\n clients, so unit tests pass mocks and never touch AWS.\n\n Default OFF: live SageMaker / EventBridge calls are operator-gated. With\n ``enabled=False`` the adapter's start/poll/register/schedule methods\n short-circuit and never construct a boto3 client.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable SageMaker scheduled-retraining orchestration. Live SageMaker \"\n \"/ EventBridge calls are operator-gated; default off.\"\n ),\n )\n mode: str = Field(\n \"training_job\",\n description=(\n \"Retraining run mode: 'training_job' (create_training_job) or \"\n \"'pipeline' (start_pipeline_execution).\"\n ),\n )\n region: str = Field(\n \"\",\n description=\"AWS region for the boto3 clients (empty => default chain).\",\n )\n training_image: str = Field(\n \"\",\n description=\"ECR image URI for the training container (training_job mode).\",\n )\n role_arn: str = Field(\n \"\",\n description=\"IAM role ARN SageMaker assumes for the retraining run.\",\n )\n instance_type: str = Field(\n \"ml.m5.xlarge\",\n description=\"Training instance type (training_job mode).\",\n )\n instance_count: int = Field(\n 1,\n description=\"Number of training instances (training_job mode).\",\n )\n volume_size_gb: int = Field(\n 30,\n description=\"Training EBS volume size in GB (training_job mode).\",\n )\n max_runtime_seconds: int = Field(\n 86400,\n description=\"Max training-job runtime in seconds before SageMaker stops it.\",\n )\n s3_artifact_bucket: str = Field(\n \"\",\n description=(\n \"S3 bucket the retraining run writes the model artifact to (the same \"\n \"location the model-artifact init/sidecar reads). Empty => no output.\"\n ),\n )\n s3_artifact_prefix: str = Field(\n \"routeiq/routing-models\",\n description=\"S3 key prefix under the artifact bucket for the model output.\",\n )\n s3_input_uri: str = Field(\n \"\",\n description=\"Optional S3 URI of the training input dataset channel.\",\n )\n pipeline_name: str = Field(\n \"routeiq-routing-retrain\",\n description=\"SageMaker Pipeline name (pipeline mode).\",\n )\n schedule_expression: str = Field(\n \"rate(7 days)\",\n description=(\n \"EventBridge cron/rate expression for the retraining schedule rule \"\n \"(e.g. 'rate(7 days)' or 'cron(0 3 ? * SUN *)').\"\n ),\n )\n schedule_rule_name: str = Field(\n \"routeiq-routing-retrain\",\n description=\"EventBridge rule name for the retraining schedule.\",\n )\n job_name_prefix: str = Field(\n \"routeiq-routing-retrain\",\n description=\"Prefix for auto-generated SageMaker training-job names.\",\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H + Cluster M).\n\n Bundles the MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), shadow/mirror canary traffic (RouteIQ-4fd1), upstream\n LiteLLM router delegation (RouteIQ-8539), SageMaker registry/experiments\n (RouteIQ-93e9), and the offline eval harness (RouteIQ-8d24). All are\n independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``,\n ``ROUTEIQ_MLOPS__UPSTREAM_ROUTER__ENABLED``,\n ``ROUTEIQ_MLOPS__SAGEMAKER__ENABLED``,\n ``ROUTEIQ_MLOPS__OFFLINE_EVAL__ENABLED``,\n ``ROUTEIQ_MLOPS__FAILURE_CAPTURE__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n upstream_router: MLOpsUpstreamRouterSettings = Field(\n default_factory=MLOpsUpstreamRouterSettings, # type: ignore[arg-type]\n description=\"Upstream LiteLLM adaptive/quality/auto router delegation.\",\n )\n sagemaker: MLOpsSageMakerSettings = Field(\n default_factory=MLOpsSageMakerSettings, # type: ignore[arg-type]\n description=\"SageMaker Model Registry + Experiments integration.\",\n )\n offline_eval: MLOpsOfflineEvalSettings = Field(\n default_factory=MLOpsOfflineEvalSettings, # type: ignore[arg-type]\n description=\"Offline eval harness over a versioned golden dataset.\",\n )\n failure_capture: MLOpsFailureCaptureSettings = Field(\n default_factory=MLOpsFailureCaptureSettings, # type: ignore[arg-type]\n description=\"Low-rate failure/timeout-path eval sample capture.\",\n )\n retraining: MLOpsRetrainingSettings = Field(\n default_factory=MLOpsRetrainingSettings, # type: ignore[arg-type]\n description=\"SageMaker scheduled-retraining orchestration (RouteIQ-8a24).\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n include_marketplace_endpoints: bool = Field(\n False,\n description=(\n \"RouteIQ-7105: also enumerate Amazon Bedrock Marketplace / mantle \"\n \"custom-deployment endpoints (``ListMarketplaceModelEndpoints``) and \"\n \"register each REGISTERED endpoint ARN as a ``model_list`` arm \"\n \"(``bedrock/<endpointArn>``). This extends the serverless-FM scan to \"\n \"cover operator-deployed Marketplace models (the bedrock-mantle \"\n \"PrivateLink custom endpoints). Default OFF -- byte-stable: the extra \"\n \"control call is opt-in and adds no arms until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__INCLUDE_MARKETPLACE_ENDPOINTS``.\"\n ),\n )\n synthesis_mode: BedrockSynthesisMode = Field(\n BedrockSynthesisMode.DISTINCT,\n description=(\n \"RouteIQ-77e8: how discovered arms fold into ``model_list``. \"\n \"``distinct`` (default, byte-stable) keeps one ``model_name`` per \"\n \"arm; ``auto_group`` collapses every arm onto the single \"\n \"``auto_group_name`` group; ``logical_groups`` emits one group per \"\n \"LOGICAL model (e.g. ``anthropic.claude-sonnet-4``) fanned across its \"\n \"global/geo/regional/mantle arms (better observability than \"\n \"``auto_group``). When set this SUPERSEDES the legacy ``auto_group`` \"\n \"bool; ``auto_group=True`` with the default ``synthesis_mode`` is \"\n \"still honoured for back-compat (it maps to ``auto_group``). Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__SYNTHESIS_MODE``.\"\n ),\n )\n full_bedrock_coverage: bool = Field(\n False,\n description=(\n \"RouteIQ-77e8: rolled-up FULL-COVERAGE switch. When True, RouteIQ \"\n \"routes across the ENTIRE Bedrock + Bedrock-Marketplace (mantle) \"\n \"surface as one operator action -- it implies ``enabled=True``, \"\n \"``include_marketplace_endpoints=True``, and \"\n \"``synthesis_mode=logical_groups`` (unless ``synthesis_mode`` was set \"\n \"explicitly to a non-default value, which wins), with the \"\n \"global>geo>regional profile preference already on by default \"\n \"(``residency_constraint=False``). So the operator flips ONE flag \"\n \"instead of four to get intelligence-first routing over every \"\n \"serverless FM + registered marketplace endpoint in every source \"\n \"region. Default OFF -- byte-stable: nothing changes until enabled. \"\n \"Env: ``ROUTEIQ_BEDROCK_DISCOVERY__FULL_BEDROCK_COVERAGE``.\"\n ),\n )\n drift_metric_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-9a42: emit a model-catalogue DRIFT metric \"\n \"(``gateway.bedrock.catalogue_drift``) measuring \"\n \"models_available (discovered) vs models_configured (the live \"\n \"model_list) so a CloudWatch alarm can fire when the deployed \"\n \"catalogue drifts from what Bedrock actually offers. Default OFF -- \"\n \"byte-stable: no metric is recorded until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_METRIC_ENABLED``.\"\n ),\n )\n drift_check_interval_seconds: float = Field(\n 900.0,\n ge=30.0,\n le=86400.0,\n description=(\n \"RouteIQ-9a42: interval (seconds) between periodic leader-gated \"\n \"catalogue-drift checks. Only consulted when \"\n \"``drift_metric_enabled`` AND ``enabled`` are True; otherwise the \"\n \"periodic task never starts (byte-stable). Default 900s (15 min). \"\n \"Env: ``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_CHECK_INTERVAL_SECONDS``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n # ------------------------------------------------------------------\n # Effective-setting resolution (RouteIQ-77e8 full-coverage rollup)\n # ------------------------------------------------------------------\n # These compute the EFFECTIVE values a scan should use after applying the\n # ``full_bedrock_coverage`` rollup, WITHOUT mutating the stored fields (so\n # the persisted config stays exactly what the operator wrote). They are the\n # single source of truth for the rollup -- both ``merge_bedrock_discovered_models``\n # and any other caller resolve through them.\n\n @property\n def effective_enabled(self) -> bool:\n \"\"\"Discovery runs when explicitly enabled OR full-coverage is on.\"\"\"\n return bool(self.enabled or self.full_bedrock_coverage)\n\n @property\n def effective_include_marketplace_endpoints(self) -> bool:\n \"\"\"Full-coverage forces marketplace/mantle endpoints into the scan.\"\"\"\n return bool(self.include_marketplace_endpoints or self.full_bedrock_coverage)\n\n @property\n def effective_synthesis_mode(self) -> \"BedrockSynthesisMode\":\n \"\"\"Resolve the synthesis mode after rollup + legacy ``auto_group`` map.\n\n Precedence (highest first):\n 1. An explicit non-default ``synthesis_mode`` (operator chose it) wins\n outright -- even under full-coverage.\n 2. ``full_bedrock_coverage`` -> ``logical_groups`` (one group per\n logical model, the recommended full-coverage shape).\n 3. Legacy ``auto_group=True`` -> ``auto_group`` (back-compat: the old\n bool still means \"collapse onto one group\" when ``synthesis_mode``\n was left at its ``distinct`` default).\n 4. Otherwise the stored ``synthesis_mode`` (``distinct`` default).\n \"\"\"\n if self.synthesis_mode is not BedrockSynthesisMode.DISTINCT:\n return self.synthesis_mode\n if self.full_bedrock_coverage:\n return BedrockSynthesisMode.LOGICAL_GROUPS\n if self.auto_group:\n return BedrockSynthesisMode.AUTO_GROUP\n return BedrockSynthesisMode.DISTINCT\n\n\nclass BedrockRequestLeversSettings(BaseModel):\n \"\"\"Per-request Bedrock Converse levers (data plane), default-OFF.\n\n Drives :class:`litellm_llmrouter.gateway.plugins.bedrock_request_levers\n .BedrockRequestLeversPlugin`, which mutates the outbound request on the\n correct litellm seam (``CustomLogger.async_pre_call_deployment_hook``) so\n the mutations actually reach the Bedrock ``Converse`` call args.\n\n Three independently-gated levers (each a byte-stable no-op when off):\n\n - ``request_metadata`` (RouteIQ-294a): forward tenant identity\n (workspace / key) as Bedrock ``requestMetadata`` tags for cost\n attribution. Lands as the top-level ``requestMetadata`` completion kwarg,\n which litellm's converse transform maps from ``non_default_params``.\n - ``team_callbacks`` (RouteIQ-9cd8): set per-team logging sinks as the\n TOP-LEVEL request kwargs ``callbacks`` / ``success_callback`` /\n ``failure_callback`` (where litellm sources dynamic per-request\n callbacks), NOT ``metadata[callbacks]`` (never read). The global\n registration is unaffected.\n - ``cache_point`` (RouteIQ-b9ee): drive Bedrock prompt-caching via\n litellm's ``cache_control_injection_points`` mechanism (system-prefix\n message injection + tool_config cachePoint), NOT a bare ``tools[]``\n ``cachePoint`` entry (dropped by the converse transform).\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_BEDROCK_LEVERS__REQUEST_METADATA``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACKS``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_POINT``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_SYSTEM``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_TOOLS``,\n ``ROUTEIQ_BEDROCK_LEVERS__METADATA_PREFIX``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACK_MAP`` (JSON\n ``{\"team-a\": {\"success_callback\": [\"s3\"]}}``).\n \"\"\"\n\n request_metadata: bool = Field(\n False,\n description=(\n \"RouteIQ-294a: forward tenant identity (workspace_id / key_id) as \"\n \"Bedrock requestMetadata tags for cost attribution. Default OFF -- \"\n \"byte-stable: no requestMetadata kwarg is added until enabled.\"\n ),\n )\n metadata_prefix: str = Field(\n \"routeiq_\",\n description=(\n \"Key prefix for the injected requestMetadata tags (Bedrock keys must \"\n \"match [a-zA-Z0-9 :_@$#=/+,.-]). Only used when request_metadata=True.\"\n ),\n )\n team_callbacks: bool = Field(\n False,\n description=(\n \"RouteIQ-9cd8: set per-team logging sinks as TOP-LEVEL request kwargs \"\n \"callbacks/success_callback/failure_callback (the seam litellm reads \"\n \"dynamic per-request callbacks from). Default OFF; the global \"\n \"registration is untouched.\"\n ),\n )\n team_callback_map: dict[str, dict[str, list[str]]] = Field(\n default_factory=dict,\n description=(\n \"Map of team_id -> {success_callback|failure_callback|callbacks: \"\n '[sink names]}. e.g. {\"team-a\": {\"success_callback\": [\"s3\"]}}. Only '\n \"applied when team_callbacks=True and the request resolves to a \"\n \"mapped team.\"\n ),\n )\n cache_point: bool = Field(\n False,\n description=(\n \"RouteIQ-b9ee: enable Bedrock prompt-caching cachePoint injection via \"\n \"litellm cache_control_injection_points. Default OFF -- byte-stable.\"\n ),\n )\n cache_system: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a system-prefix cachePoint (message \"\n \"injection point, role=system). Default on.\"\n ),\n )\n cache_tools: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a tool_config cachePoint so the tool \"\n \"schema prefix is cached. Default on. Only takes effect when the \"\n \"request carries tools.\"\n ),\n )\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\nclass EngineMetricsSettings(BaseModel):\n \"\"\"Self-hosted-engine ``/metrics`` scrape configuration (default OFF).\n\n Drives :mod:`litellm_llmrouter.engine_metrics`, the credless scraper that\n GETs a self-hosted inference engine's Prometheus ``/metrics`` (vLLM, vLLM\n Production Stack, AIBrix, llm-d -- all serve the ``vllm:*`` family) and\n parses the queue-depth / KV-cache-pressure gauges\n (``vllm:num_requests_waiting``, ``vllm:kv_cache_usage_perc`` /\n ``vllm:gpu_cache_usage_perc``, ...). Those signals feed a *future*\n KV/queue-aware Layer-1 router or an autoscaler-into-the-engine.\n\n LAYERING: this reads the engine *frontend's* aggregate ``/metrics`` so\n Layer-1 can decide whether to send more load to this engine arm; it never\n scrapes individual worker pods (that would collapse the two-layer model --\n see ``docs/architecture/aws-rearchitecture/51-multinode-large-model-serving.md``\n Part 3).\n\n Default DISABLED (opt-in): with ``enabled=False`` the scraper is a byte-stable\n no-op -- importing the module does no I/O, and ``scrape()`` returns an empty\n ``reachable=False`` snapshot without touching the network. Live scraping is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_ENGINE_METRICS__ENABLED``,\n ``ROUTEIQ_ENGINE_METRICS__SCRAPE_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the self-hosted-engine /metrics scrape. Default OFF -- the \"\n \"scraper no-ops and does zero network I/O until an operator opts in \"\n \"(live scrape is operator-gated).\"\n ),\n )\n scrape_timeout: float = Field(\n 2.0,\n ge=0.1,\n le=30.0,\n description=(\n \"Per-scrape HTTP GET timeout in seconds. An engine that does not \"\n \"answer within this budget yields an empty (reachable=False) \"\n \"snapshot, never an exception.\"\n ),\n )\n kv_aware_routing_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-08d6/6a89: enable the KV-cache-/queue-aware routing path for \"\n \"self-hosted engine arms. When ON the router scrapes each candidate \"\n \"arm's engine /metrics (vllm:num_requests_waiting, kv_cache_usage_perc) \"\n \"and selects the LEAST-LOADED arm. Requires ``enabled`` (the scraper) \"\n \"to also be True. Default OFF -- byte-stable: the KV-aware pre-step is \"\n \"skipped entirely and routing is unchanged. Env: \"\n \"``ROUTEIQ_ENGINE_METRICS__KV_AWARE_ROUTING_ENABLED``.\"\n ),\n )\n kv_cache_usage_weight: float = Field(\n 1.0,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's KV-cache usage fraction (0..1) when \"\n \"scoring engine load for KV-aware routing. Higher => avoid \"\n \"cache-pressured arms more aggressively.\"\n ),\n )\n waiting_queue_weight: float = Field(\n 0.1,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's num_requests_waiting (queue depth) when \"\n \"scoring engine load for KV-aware routing. The load score is \"\n \"kv_cache_usage_weight*kv_usage + waiting_queue_weight*num_waiting.\"\n ),\n )\n\n\nclass MultinodeAffinitySettings(BaseModel):\n \"\"\"Multinode engine-affinity passthrough configuration (default OFF).\n\n Drives the LIVE wiring of :mod:`litellm_llmrouter.engine_affinity` +\n :mod:`litellm_llmrouter.conversation_affinity` into the request path via\n :class:`~litellm_llmrouter.gateway.plugin_callback_bridge.PluginCallbackBridge`\n (RouteIQ-bdd0 + RouteIQ-3316). When enabled, the bridge's deployment hook\n derives a session/conversation affinity key from the request, looks up a\n sticky decode-worker hint, and injects ``x-worker-instance-id`` /\n ``x-routeiq-affinity-key`` headers so a multi-turn conversation lands on the\n same disaggregated decode worker; the success hook records the\n ``response_id -> deployment`` mapping for the next turn.\n\n Default DISABLED (opt-in). With ``enabled=False`` the bridge's affinity logic\n is a byte-stable no-op: no tracker lookups, no header injection, no recording.\n\n The master enable flag is the EXISTING ``ROUTEIQ_MULTINODE_AFFINITY_ENABLED``\n env var, read by :func:`engine_affinity.multinode_affinity_enabled` (the\n single source of truth for the gate); this nested model adds only the\n disaggregation-default sub-flag.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MULTINODE_AFFINITY__DISAGG_DEFAULT_REMOTE_DECODE``,\n ``ROUTEIQ_MULTINODE_AFFINITY__DISAGG_DEFAULT_REMOTE_PREFILL``.\n \"\"\"\n\n disagg_default_remote_decode: bool = Field(\n False,\n description=(\n \"RouteIQ-3316: when set, every request (under the master \"\n \"ROUTEIQ_MULTINODE_AFFINITY_ENABLED gate) carries do_remote_decode=true \"\n \"to the engine as a forward-compat disaggregation-coordination \"\n \"passthrough. RouteIQ makes NO disaggregation decision -- it only \"\n \"carries the signal. Default OFF (byte-stable: non-disagg requests add \"\n \"zero params). A per-request metadata flag overrides this default.\"\n ),\n )\n disagg_default_remote_prefill: bool = Field(\n False,\n description=(\n \"RouteIQ-3316: when set, every request (under the master \"\n \"ROUTEIQ_MULTINODE_AFFINITY_ENABLED gate) carries do_remote_prefill=true \"\n \"to the engine as a forward-compat disaggregation-coordination \"\n \"passthrough. Default OFF (byte-stable). A per-request metadata flag \"\n \"overrides this default.\"\n ),\n )\n\n\n# ============================================================================\n# Cluster TC-WIRE additions (fresh region — do not reformat blocks above)\n# ============================================================================\n\n\nclass LogArchivalSettings(BaseModel):\n \"\"\"S3 archival of full request/response logs (RouteIQ-6702).\n\n Opt-in, PII-gated long-term retention of full request/response payloads to an\n S3 bucket (compliance / audit / offline-eval corpus). Default DISABLED: when\n ``enabled=False`` the gateway registers no archival callback, builds no boto3\n client, and writes nothing to S3 (byte-stable). When enabled, a successful\n LLM call's request/response is streamed to a date-partitioned, lifecycle-tier\n -friendly S3 key the operator's S3 lifecycle policy transitions by tier.\n\n PII gate: archival of full payloads is gated behind an explicit\n ``pii_acknowledged`` flag so a deployment cannot silently begin persisting\n prompt/response text. Both ``enabled`` AND ``pii_acknowledged`` must be true\n for the live callback to be wired.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix), e.g.\n ``ROUTEIQ_LOG_ARCHIVAL__ENABLED=true``. The legacy flat\n ``ROUTEIQ_LOG_ARCHIVAL_ENABLED`` / ``ROUTEIQ_LOG_ARCHIVAL_BUCKET`` etc. read\n directly by :mod:`litellm_llmrouter.log_archival` are still honoured by the\n archiver itself; this block drives the LIVE callback wiring decision.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable S3 archival of full request/response logs. Default off \"\n \"(no callback, no boto3 client, byte-stable).\"\n ),\n )\n pii_acknowledged: bool = Field(\n False,\n description=(\n \"Explicit acknowledgement that archiving full payloads persists \"\n \"prompt/response text (PII). The live archival callback is wired only \"\n \"when this AND ``enabled`` are both true.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n # Per-workspace cross-account arm synthesis (RouteIQ-c6e9; default off).\n workspace_arm_synthesis: WorkspaceArmSynthesisSettings = Field(\n default_factory=WorkspaceArmSynthesisSettings, # type: ignore[arg-type]\n description=(\n \"Per-workspace cross-account Bedrock arm synthesis settings \"\n \"(RouteIQ-c6e9; default off / byte-stable).\"\n ),\n )\n # AWS Secrets Manager credential vault (RouteIQ-3d33; default off).\n secrets_vault: SecretsVaultSettings = Field(\n default_factory=SecretsVaultSettings, # type: ignore[arg-type]\n description=(\n \"AWS Secrets Manager credential vault settings (RouteIQ-3d33; \"\n \"default off / pass-through).\"\n ),\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n multinode_affinity: MultinodeAffinitySettings = Field(\n default_factory=MultinodeAffinitySettings, # type: ignore[arg-type]\n description=\"Multinode engine-affinity passthrough settings (RouteIQ-bdd0/3316).\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n api_surfaces: ApiSurfacesSettings = Field(\n default_factory=ApiSurfacesSettings, # type: ignore[arg-type]\n description=(\n \"First-class API surfaces, persisted spend, and self-service key \"\n \"settings (cluster N: RouteIQ-e48a / 67fe / 3215; all default on / \"\n \"byte-stable).\"\n ),\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n bedrock_levers: BedrockRequestLeversSettings = Field(\n default_factory=BedrockRequestLeversSettings, # type: ignore[arg-type]\n description=(\n \"Per-request Bedrock Converse levers (requestMetadata / team \"\n \"callbacks / cachePoint) on the pre-call mutation seam (default off).\"\n ),\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n region_routing: RegionRoutingSettings = Field(\n default_factory=RegionRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Per-request region-aware / data-residency candidate-filter settings \"\n \"(default off / identity).\"\n ),\n )\n capability_routing: CapabilityRoutingSettings = Field(\n default_factory=CapabilityRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Intelligence-first capability-tier floor candidate-filter settings \"\n \"(RouteIQ-8e37; default off / identity).\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n engine_metrics: EngineMetricsSettings = Field(\n default_factory=EngineMetricsSettings, # type: ignore[arg-type]\n description=(\n \"Self-hosted-engine /metrics scrape settings (vLLM / Production \"\n \"Stack / AIBrix / llm-d queue-depth + KV-cache gauges; default off).\"\n ),\n )\n log_archival: LogArchivalSettings = Field(\n default_factory=LogArchivalSettings, # type: ignore[arg-type]\n description=(\n \"S3 archival of full request/response logs (RouteIQ-6702; opt-in, \"\n \"PII-gated, default off / byte-stable).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_cluster_w2_aliases(cls, data: Any) -> Any:\n \"\"\"Map cluster-W2 flat env vars onto their nested settings fields.\n\n pydantic-settings won't resolve a flat top-level env var onto a nested\n ``BaseModel`` field, so (mirroring ``_map_flat_iam_aliases``) inject the\n flat values here, before validation. All DEFAULT OFF / unset, so the\n default path is byte-stable.\n\n * ``ROUTEIQ_GOVERNANCE_BACKEND`` (RouteIQ-a865) -> ``governance.backend``\n * ``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS_ENABLED`` (RouteIQ-c6e9) ->\n ``workspace_arm_synthesis.enabled``\n * ``ROUTEIQ_SECRETS_VAULT_ENABLED`` (RouteIQ-3d33) ->\n ``secrets_vault.enabled``\n\n The nested ``ROUTEIQ_<SECTION>__<FIELD>`` form WINS if both are set.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n def _set_nested(section: str, field: str, value: Any) -> None:\n current = data.get(section)\n if current is None:\n data[section] = {field: value}\n elif isinstance(current, dict):\n current.setdefault(field, value)\n elif isinstance(current, BaseModel):\n # Only fill when the field is still at its falsy default.\n if not getattr(current, field, None):\n data[section] = current.model_copy(update={field: value})\n\n backend = _os.environ.get(\"ROUTEIQ_GOVERNANCE_BACKEND\")\n if backend:\n _set_nested(\"governance\", \"backend\", backend.strip().lower())\n\n arm_enabled = _os.environ.get(\"ROUTEIQ_WORKSPACE_ARM_SYNTHESIS_ENABLED\")\n if arm_enabled is not None and _truthy(arm_enabled):\n _set_nested(\"workspace_arm_synthesis\", \"enabled\", True)\n\n vault_enabled = _os.environ.get(\"ROUTEIQ_SECRETS_VAULT_ENABLED\")\n if vault_enabled is not None and _truthy(vault_enabled):\n _set_nested(\"secrets_vault\", \"enabled\", True)\n\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "3b52d2a2b94bbe326385914d2e9e4d2058c62061": {"t": 1785769068.492323, "n": 148332, "h": "dadf081589deffb8", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass BedrockSynthesisMode(str, Enum):\n \"\"\"How Bedrock auto-discovery folds discovered arms into ``model_list``.\n\n - ``distinct`` — one distinct ``model_name`` per discovered arm (the\n byte-stable historical default; the operator-authored\n list is only extended, never collapsed).\n - ``auto_group`` — every discovered serverless arm (across ALL providers\n and tiers) collapses onto ONE shared ``model_name``\n (``auto_group_name``) so a single routing group fans\n out over the whole Bedrock surface.\n - ``logical_groups`` — one group PER LOGICAL model (e.g.\n ``anthropic.claude-sonnet-4``) fanned across its\n global/geo/regional/mantle arms; better observability\n than ``auto_group`` (which collapses everything to one\n name) while still letting the bandit cascade per group.\n \"\"\"\n\n DISTINCT = \"distinct\"\n AUTO_GROUP = \"auto_group\"\n LOGICAL_GROUPS = \"logical_groups\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n retry_consumer_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-3ff5: enable the mode-(a) cascade RETRY consumer -- a \"\n \"component that reads the ``routeiq_cascade`` ladder on the cheapest \"\n \"rung's response, scores the response confidence, and RE-ISSUES the \"\n \"request pinned to the NEXT (stronger) rung when confidence is below \"\n \"``confidence_threshold``, climbing up to ``max_rungs`` rungs. Default \"\n \"OFF -- byte-stable: with it off a single cheapest-rung completion is \"\n \"returned unchanged. Env: \"\n \"``ROUTEIQ_COST_CASCADE__RETRY_CONSUMER_ENABLED``.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass RegionRoutingSettings(BaseModel):\n \"\"\"Per-request region-aware / data-residency candidate-filter configuration\n (RouteIQ-60cc).\n\n Bedrock cross-region inference profiles are already used STATICALLY (each\n ``model_list`` arm pins ``litellm_params.aws_region_name`` / a geo-prefixed\n inference profile). This block adds a PER-REQUEST region preference + a hard\n residency constraint on top, applied as a PRE-SCORING candidate filter (the\n same ``filter_routable_candidates`` seam every RouteIQ strategy already calls,\n so it composes with gov-ban + cooldown and benefits EVERY strategy -- no new\n strategy subclass needed).\n\n The requested region is resolved per request, FIRST match wins:\n\n 1. The request **header** named by :attr:`region_header` (read\n case-insensitively from ``request_kwargs['headers']`` /\n ``metadata['headers']``) -- e.g. ``X-RouteIQ-Region: eu``.\n 2. An explicit ``region`` / ``residency_region`` key in\n ``request_kwargs`` / ``metadata`` (e.g. a governance-workspace region the\n auth layer stamped onto the request metadata).\n\n A request is \"constrained\" (HARD residency) when EITHER the resolved value\n came with a residency flag (``residency`` / ``data_residency`` truthy in the\n same source) OR :attr:`hard_residency_default` is True. A constrained request\n is NEVER routed out-of-region: if no in-region candidate exists the candidate\n set is emptied (the strategy then returns ``None`` / triggers a fallback) --\n a residency violation must never silently leak. An UNCONSTRAINED request\n PREFERS in-region but falls back to the full set when no in-region arm exists.\n\n A candidate's region is read from ``litellm_params.aws_region_name`` (the\n field every Bedrock arm already sets), then normalized through\n :attr:`region_map` so a request token (``\"eu\"``) can map to one or more\n concrete AWS regions (``[\"eu-west-1\", \"eu-central-1\"]``). When a request\n token is absent from ``region_map`` it is matched verbatim (so an\n ``aws_region_name``-equal request token works with zero map config).\n\n Env vars (``__`` nested delimiter): ``ROUTEIQ_REGION_ROUTING__ENABLED``,\n ``ROUTEIQ_REGION_ROUTING__REGION_HEADER``,\n ``ROUTEIQ_REGION_ROUTING__REGION_MAP`` (JSON map),\n ``ROUTEIQ_REGION_ROUTING__HARD_RESIDENCY_DEFAULT``.\n\n Default OFF / identity: when ``enabled`` is False the filter is a byte-stable\n no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request region-aware / data-residency candidate \"\n \"pre-filter. Default off -> identity (byte-stable no-op).\"\n ),\n )\n region_header: str = Field(\n \"X-RouteIQ-Region\",\n description=(\n \"Request header carrying the desired region/residency token (read \"\n \"case-insensitively). Checked before the request_kwargs/metadata \"\n \"region keys.\"\n ),\n )\n region_map: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request region TOKEN -> list of concrete AWS regions that \"\n 'satisfy it (e.g. {\"eu\": [\"eu-west-1\", \"eu-central-1\"]}). A token '\n \"absent from the map is matched verbatim against the candidate \"\n \"``aws_region_name``, so an exact-region token needs no map entry.\"\n ),\n )\n hard_residency_default: bool = Field(\n False,\n description=(\n \"When True, EVERY region-constrained request is treated as a HARD \"\n \"residency constraint (never routed out-of-region) even without a \"\n \"per-request residency flag. When False (default) only requests that \"\n \"carry an explicit residency flag are hard-constrained; an \"\n \"unflagged region is a soft preference (in-region first, full-set \"\n \"fallback).\"\n ),\n )\n\n\nclass CapabilityRoutingSettings(BaseModel):\n \"\"\"INTELLIGENCE-FIRST capability-tier floor (RouteIQ-8e37).\n\n Makes the RL router pick the most CAPABLE model for the task, not the\n cheapest, by enforcing a per-request capability-tier FLOOR as a PRE-SCORING\n candidate filter on the SAME ``filter_routable_candidates`` seam every\n RouteIQ strategy already calls (the region filter — RouteIQ-60cc — is the\n precedent). It composes with gov-ban + cooldown + region and benefits EVERY\n strategy (default / ML / bandit / centroid / FALLBACK) with no new strategy\n subclass and no per-path edit.\n\n Each model carries an ordered capability TIER — ``fast`` < ``advanced`` <\n ``expert`` (see :attr:`tier_order`). The request DIFFICULTY (resolved from\n the warm centroid complexity classifier + reasoning markers — never a cold\n model load on the hot path) maps to a REQUIRED minimum tier: a simple task\n accepts any tier; a complex / reasoning task requires ``expert``. When\n gating is :attr:`enabled` and the request needs ``>=`` a tier, models BELOW\n that tier are DROPPED from the candidate set BEFORE scoring, so a reasoning\n task NEVER scores a sub-tier model — even in fallback / exploration.\n\n Tiers are CONFIG-DRIVEN: :attr:`model_tiers` overrides the per-family\n defaults (:data:`litellm_llmrouter.centroid_routing.MODEL_CAPABILITY_TIERS`).\n An UNKNOWN model defaults to :attr:`default_tier` (``advanced``, a safe\n MIDDLE tier) so it is never WRONGLY excluded by a tier floor.\n\n Capability is a PREFERENCE floor, not a hard residency deny: in the default\n SOFT mode, if the floor would EMPTY the candidate set (no capable model\n available) it DEGRADES GRACEFULLY — logs and falls back to the full set\n rather than returning None / 500. Set :attr:`strict` to fail-closed-to-empty\n instead (the strategy then yields None / triggers a fallback).\n\n Env vars (``__`` nested delimiter):\n ``ROUTEIQ_CAPABILITY_ROUTING__ENABLED``,\n ``ROUTEIQ_CAPABILITY_ROUTING__STRICT``,\n ``ROUTEIQ_CAPABILITY_ROUTING__DEFAULT_TIER``,\n ``ROUTEIQ_CAPABILITY_ROUTING__MODEL_TIERS`` (JSON map),\n ``ROUTEIQ_CAPABILITY_ROUTING__COMPLEX_MIN_TIER``,\n ``ROUTEIQ_CAPABILITY_ROUTING__SIMPLE_MIN_TIER``.\n\n Default OFF / identity: when :attr:`enabled` is False the filter is a\n byte-stable no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request capability-tier FLOOR candidate pre-filter \"\n \"(intelligence-first routing). Default off -> identity (byte-stable \"\n \"no-op).\"\n ),\n )\n strict: bool = Field(\n False,\n description=(\n \"When True, the capability floor is HARD: if no candidate meets the \"\n \"required tier the set is EMPTIED (strategy yields None / fallback). \"\n \"When False (default) the floor DEGRADES GRACEFULLY — it logs and \"\n \"returns the full set rather than emptying it (capability is a \"\n \"PREFERENCE floor, not a residency deny; never 500 on an empty set).\"\n ),\n )\n tier_order: List[str] = Field(\n default_factory=lambda: [\"fast\", \"advanced\", \"expert\"],\n description=(\n \"Ordered capability tiers from least to most capable. A model's tier \"\n \"is compared by its INDEX here; a required tier of ``expert`` drops \"\n \"every model whose tier index is below ``expert``'s. Operators may \"\n \"extend / reorder this for custom fleets.\"\n ),\n )\n default_tier: str = Field(\n \"advanced\",\n description=(\n \"Tier assigned to a model with NO known / configured tier. A SAFE \"\n \"MIDDLE tier (``advanced``) so an unknown model is never WRONGLY \"\n \"excluded by a tier floor (it clears a ``fast``/``advanced`` floor \"\n \"and is only dropped by an ``expert`` floor).\"\n ),\n )\n model_tiers: Dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Operator override map of model id (or family substring) -> tier \"\n 'name (e.g. {\"my-custom-7b\": \"fast\", \"my-frontier\": \"expert\"}). '\n \"Checked BEFORE the per-family defaults so an operator can pin any \"\n \"model's tier. A tier value not in ``tier_order`` falls back to \"\n \"``default_tier``.\"\n ),\n )\n complex_min_tier: str = Field(\n \"expert\",\n description=(\n \"Required MINIMUM tier for a COMPLEX / reasoning-difficulty request \"\n \"(centroid 'complex' tier OR reasoning markers detected). Default \"\n \"``expert`` — a hard-reasoning task never scores a sub-expert model.\"\n ),\n )\n simple_min_tier: str = Field(\n \"fast\",\n description=(\n \"Required MINIMUM tier for a SIMPLE-difficulty request. Default \"\n \"``fast`` — a simple task accepts ANY tier (the lowest floor).\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n # -- Durable-store backend selection (RouteIQ-a865) ----------------------\n # Which durable backend ``get_governance_store()`` returns. ``file``\n # (default) keeps the file-backed in-memory + Aurora path unchanged\n # (byte-stable); ``dynamodb`` selects the single-table DynamoDB store\n # (``governance_store_dynamodb.DynamoDBGovernanceStore``). The DynamoDB\n # store ALSO reads the flat ``ROUTEIQ_GOVERNANCE_BACKEND`` env directly\n # (its ``governance_backend()`` helper) so both forms agree; this field\n # surfaces the choice in the typed settings surface for discoverability.\n backend: str = Field(\n \"file\",\n description=(\n \"Durable governance store backend: ``file`` (default; file-backed \"\n \"in-memory + optional Aurora) or ``dynamodb`` (single-table \"\n \"DynamoDB). Default ``file`` -> byte-stable. Env (flat, the source \"\n \"of truth the DynamoDB store reads): ``ROUTEIQ_GOVERNANCE_BACKEND``.\"\n ),\n )\n\n\n# ============================================================================\n# Per-workspace cross-account arm synthesis (RouteIQ-c6e9) -- fresh region\n# ============================================================================\n\n\nclass WorkspaceArmSynthesisSettings(BaseModel):\n \"\"\"Per-workspace cross-account Bedrock arm synthesis (RouteIQ-c6e9).\n\n When a workspace declares an ``aws_role_arn`` (its models live in a DIFFERENT\n AWS account), RouteIQ can synthesize per-workspace Bedrock deployment arms\n (STS AssumeRole at call time) and merge them into the live ``model_list`` at\n startup. Default OFF -> byte-stable no-op (no arms synthesized, the\n operator-authored model_list is untouched).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Synthesize per-workspace cross-account Bedrock arms (from each \"\n \"registered workspace's ``aws_role_arn``) and merge them into the \"\n \"live ``model_list`` at startup. Default OFF -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS_ENABLED`` (flat) or \"\n \"``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS__ENABLED`` (nested).\"\n ),\n )\n default_region: Optional[str] = Field(\n None,\n description=(\n \"Fallback AWS region for synthesized arms when a workspace does not \"\n \"carry its own ``aws_region``. Falls back to ``AWS_REGION`` resolution \"\n \"in the synthesizer when unset. \"\n \"Env: ``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS__DEFAULT_REGION``.\"\n ),\n )\n\n\n# ============================================================================\n# AWS Secrets Manager credential vault (RouteIQ-3d33) -- fresh region\n# ============================================================================\n\n\nclass SecretsVaultSettings(BaseModel):\n \"\"\"AWS Secrets Manager credential vault for provider keys (RouteIQ-3d33).\n\n Surfaces the ``secrets_vault.SecretsVault`` backend in the typed settings\n surface. The vault resolves ``aws-secrets://<id>[#<json-key>]`` references to\n real provider keys at lookup time. Default OFF -> the vault is a transparent\n pass-through (no boto3 import, no AWS call) so a default deployment is\n byte-stable. The vault module ALSO reads the flat env vars directly so both\n forms agree.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the AWS Secrets Manager credential vault. When OFF (default) \"\n \"``resolve()`` is a transparent pass-through (no AWS call). \"\n \"Env (the source of truth the vault module reads): \"\n \"``ROUTEIQ_SECRETS_VAULT_ENABLED``.\"\n ),\n )\n region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the Secrets Manager client. Falls back to \"\n \"``AWS_REGION`` (default ``us-east-1``) in the vault when unset. \"\n \"Env: ``ROUTEIQ_SECRETS_VAULT__REGION``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n access_group_enforcement: bool = Field(\n False,\n description=(\n \"RouteIQ-2fa1: enforce per-key/per-tool MCP access groups on the LIVE \"\n \"invocation path. When True, invoking a tool whose backing server \"\n \"declares ``metadata['access_groups']`` requires the caller to present \"\n \"at least one matching access group; a non-member call is REJECTED at \"\n \"``invoke_tool`` (the terminal path, before any outbound MCP call). \"\n \"When False (default) the access-group metadata is purely advisory \"\n \"(used only by the discovery/registry listing filter) -- byte-stable. \"\n \"Servers with NO ``access_groups`` configured are unrestricted on \"\n \"either setting (open by default). Env: \"\n \"``ROUTEIQ_MCP__ACCESS_GROUP_ENFORCEMENT``.\"\n ),\n )\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n agentcore_memory_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-60e7: persist A2A agent/conversation state into AWS Bedrock \"\n \"AgentCore Memory after a successful agent invocation (via the live \"\n \"bedrock-agentcore-mcp plugin's CreateEvent). Default OFF -- \"\n \"byte-stable: the A2A invocation path is unchanged and issues no \"\n \"AgentCore call until an operator opts in. Env: \"\n \"``ROUTEIQ_A2A__AGENTCORE_MEMORY_ENABLED`` (or A2A_AGENTCORE_MEMORY_ENABLED).\"\n ),\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass ApiSurfacesSettings(BaseModel):\n \"\"\"First-class API surfaces, persisted spend, and self-service keys\n (RouteIQ cluster N: e48a / 67fe / 3215).\n\n All flags DEFAULT to the byte-stable behaviour:\n\n * ``bare_paths_enabled`` -- register the bare-path delegators (Responses,\n rerank, RAG, batches, vector-store, audio, images) on the parent app so\n they get RouteIQ middleware + documentation (default ON; a no-op when\n LiteLLM is not mounted).\n * ``persisted_spend_enabled`` -- aggregate the spend report from the durable\n ``governance_spend`` rollup (default ON; degrades to the zero/no-op report\n automatically when no ``DATABASE_URL`` is configured).\n * ``self_service_keys_enabled`` -- expose the user-tier self-service key\n endpoints (default ON; they are scope-isolated to the caller's identity).\n\n Env vars: ``ROUTEIQ_API_SURFACES__BARE_PATHS_ENABLED``,\n ``ROUTEIQ_API_SURFACES__PERSISTED_SPEND_ENABLED``,\n ``ROUTEIQ_API_SURFACES__SELF_SERVICE_KEYS_ENABLED``.\n \"\"\"\n\n bare_paths_enabled: bool = Field(\n True,\n description=(\n \"Register first-class bare-path API surface delegators so they get \"\n \"RouteIQ middleware + OpenAPI documentation (no-op without the /v1 \"\n \"LiteLLM mount).\"\n ),\n )\n persisted_spend_enabled: bool = Field(\n True,\n description=(\n \"Aggregate the spend report from the durable governance_spend \"\n \"rollup (degrades to a zero report when no DATABASE_URL is set).\"\n ),\n )\n self_service_keys_enabled: bool = Field(\n True,\n description=(\n \"Expose user-tier self-service key endpoints (scope-isolated to the \"\n \"caller's identity).\"\n ),\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n reward_profile: str = Field(\n \"balanced\",\n description=(\n \"INTELLIGENCE-FIRST reward profile (RouteIQ-8e37). A one-flag switch \"\n \"for the bandit's reward weights:\\n\"\n \" - ``balanced`` (DEFAULT, byte-stable): the explicit \"\n \"``w_quality`` / ``w_cost`` / ``w_latency`` below (0.5 / 0.4 / 0.1).\\n\"\n \" - ``intelligence_first`` (RECOMMENDED for the Claude-Code recipe): \"\n \"weight quality ~1.0, cost ~0, latency ~0 — within the capable tier \"\n \"the bandit LEARNS per-bucket which model is actually best from the \"\n \"LLM-judge (Opus vs MiniMax vs Kimi vs DeepSeek), not the cheapest.\\n\"\n \" - ``cost_aware``: weight cost more heavily (0.2 / 0.7 / 0.1).\\n\"\n \"Any non-default profile OVERRIDES the explicit ``w_*`` weights; \"\n \"``balanced`` leaves them untouched so existing configs are unchanged. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__REWARD_PROFILE``.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n\n def resolved_reward_weights(self) -> tuple[float, float, float]:\n \"\"\"Resolve ``(w_quality, w_cost, w_latency)`` for the active profile.\n\n ``balanced`` (default) returns the explicit ``w_quality`` / ``w_cost`` /\n ``w_latency`` UNCHANGED (byte-stable). ``intelligence_first`` and\n ``cost_aware`` are one-flag presets that OVERRIDE those weights. An\n unknown profile falls back to the explicit weights (treated as balanced)\n so a typo never silently zeroes a weight.\n \"\"\"\n profile = (self.reward_profile or \"balanced\").strip().lower()\n if profile == \"intelligence_first\":\n return (1.0, 0.0, 0.0)\n if profile == \"cost_aware\":\n return (0.2, 0.7, 0.1)\n # balanced (default) or unknown -> the explicit per-weight fields.\n return (self.w_quality, self.w_cost, self.w_latency)\n\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsFailureCaptureSettings(BaseModel):\n \"\"\"Low-rate capture from the eval COLLECT FAILURE/timeout path (RouteIQ-d365).\n\n The eval COLLECT arm only samples SUCCESS responses, so AGGREGATE/FEEDBACK\n never sees which models/strategies are error-prone. When enabled, a\n parallel, clearly-labeled, low-rate capture fires on the FAILURE path\n (``async_log_failure_event``) so the loop can downweight error-prone\n models/strategies.\n\n Captured failure samples are tagged (``outcome=\"failure\"`` +\n ``error_type``) so AGGREGATE never confuses them with successful responses.\n Default OFF and sampled at a LOW rate so a failure storm cannot flood the\n eval queue.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable low-rate capture of FAILURE/timeout-path eval samples.\",\n )\n sample_rate: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Fraction of failure events to capture (kept LOW so a failure storm \"\n \"does not flood the eval queue).\"\n ),\n )\n\n\nclass MLOpsUpstreamRouterSettings(BaseModel):\n \"\"\"Delegate routing to an upstream LiteLLM adaptive/quality/auto router\n (RouteIQ-8539).\n\n Upstream LiteLLM ships its OWN online-bandit / quality-feedback routing\n modes (``litellm.router_strategy.adaptive_router`` /\n ``quality_router`` / ``auto_router``), each configured per-deployment via the\n ``auto_router/...`` model prefix and holding an in-memory\n ``AdaptiveRouterUpdateQueue``. This block lets RouteIQ DELEGATE to one of\n those modes (alongside the RouteIQ ML strategies) and FLUSH the chosen\n router's update queue to the durable store on the MLOps cadence.\n\n Default OFF: with ``enabled=False`` RouteIQ never touches the upstream\n router registries, so this is a byte-stable no-op until an operator opts in.\n The upstream router itself must additionally be configured in the LiteLLM\n ``model_list`` (the ``auto_router/...`` deployment) -- this flag only wires\n RouteIQ's delegation + queue-flush, it does not create the deployment.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Delegate to an upstream LiteLLM adaptive/quality/auto router and \"\n \"flush its update queue to the durable store.\"\n ),\n )\n mode: str = Field(\n \"adaptive\",\n description=(\n \"Which upstream router family to delegate to: 'adaptive' \"\n \"(adaptive_router), 'quality' (quality_router), or 'auto' \"\n \"(auto_router). Selects which of the LiteLLM Router's per-name \"\n \"router dicts RouteIQ delegates to / flushes.\"\n ),\n )\n router_name: str = Field(\n \"\",\n description=(\n \"Optional specific upstream router_name (the model_list deployment \"\n \"name) to delegate to. Empty => the first registered router of the \"\n \"selected mode.\"\n ),\n )\n flush_queue: bool = Field(\n True,\n description=(\n \"On each MLOps feedback cadence, drain the selected upstream \"\n \"router's AdaptiveRouterUpdateQueue (state + session) to the durable \"\n \"store (the proxy prisma client). No-op when the mode has no queue \"\n \"(auto/quality) or no durable client is available.\"\n ),\n )\n\n\nclass MLOpsSageMakerSettings(BaseModel):\n \"\"\"SageMaker Model Registry + Experiments integration (RouteIQ-93e9).\n\n Registers a trained routing-model artifact into a SageMaker Model Package\n Group (with an approval status) and logs run metrics to SageMaker\n Experiments. Cred-free + mockable: the adapter takes an injected boto3\n sagemaker client, so unit tests pass a mock and never touch AWS.\n\n Default OFF: live SageMaker calls are operator-gated. With\n ``enabled=False`` the adapter's register/log methods short-circuit and never\n construct a boto3 client.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable SageMaker Model Registry + Experiments integration. Live \"\n \"SageMaker calls are operator-gated; default off.\"\n ),\n )\n model_package_group_name: str = Field(\n \"routeiq-routing-models\",\n description=\"SageMaker Model Package Group the routing artifact registers into.\",\n )\n approval_status: str = Field(\n \"PendingManualApproval\",\n description=(\n \"ModelApprovalStatus for the registered package \"\n \"(PendingManualApproval | Approved | Rejected).\"\n ),\n )\n experiment_name: str = Field(\n \"routeiq-routing-eval\",\n description=\"SageMaker Experiment name run metrics are logged under.\",\n )\n region: str = Field(\n \"\",\n description=\"AWS region for the boto3 sagemaker client (empty => default chain).\",\n )\n\n\nclass MLOpsOfflineEvalSettings(BaseModel):\n \"\"\"Offline eval harness over a versioned golden dataset (RouteIQ-8d24).\n\n Replays a versioned golden dataset (request -> expected-quality) through the\n eval judge / a strategy and reports per-strategy quality OFFLINE -- no live\n traffic. Default OFF; the harness is also invokable directly (tests / a CLI\n / a batch job) without flipping this flag.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the offline eval harness (also directly invokable).\",\n )\n golden_dataset_path: str = Field(\n \"\",\n description=(\n \"Path to the versioned golden dataset JSON. Empty => the bundled \"\n \"default fixture under config/golden_eval/.\"\n ),\n )\n\n\nclass MLOpsRetrainingSettings(BaseModel):\n \"\"\"SageMaker scheduled-retraining orchestration (RouteIQ-8a24).\n\n Turns the manual MLflow training step into an operator-gated, scheduled loop:\n an EventBridge cron/rate rule triggers a SageMaker Training Job / Pipeline\n execution that writes the routing-model artifact to S3; on success the\n produced artifact is handed to the EXISTING SageMaker Model Registry adapter\n (``settings.mlops.sagemaker``) so train -> register -> approve -> promote\n stays one lifecycle.\n\n Cred-free + mockable: the adapter takes injected boto3 sagemaker + events\n clients, so unit tests pass mocks and never touch AWS.\n\n Default OFF: live SageMaker / EventBridge calls are operator-gated. With\n ``enabled=False`` the adapter's start/poll/register/schedule methods\n short-circuit and never construct a boto3 client.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable SageMaker scheduled-retraining orchestration. Live SageMaker \"\n \"/ EventBridge calls are operator-gated; default off.\"\n ),\n )\n mode: str = Field(\n \"training_job\",\n description=(\n \"Retraining run mode: 'training_job' (create_training_job) or \"\n \"'pipeline' (start_pipeline_execution).\"\n ),\n )\n region: str = Field(\n \"\",\n description=\"AWS region for the boto3 clients (empty => default chain).\",\n )\n training_image: str = Field(\n \"\",\n description=\"ECR image URI for the training container (training_job mode).\",\n )\n role_arn: str = Field(\n \"\",\n description=\"IAM role ARN SageMaker assumes for the retraining run.\",\n )\n instance_type: str = Field(\n \"ml.m5.xlarge\",\n description=\"Training instance type (training_job mode).\",\n )\n instance_count: int = Field(\n 1,\n description=\"Number of training instances (training_job mode).\",\n )\n volume_size_gb: int = Field(\n 30,\n description=\"Training EBS volume size in GB (training_job mode).\",\n )\n max_runtime_seconds: int = Field(\n 86400,\n description=\"Max training-job runtime in seconds before SageMaker stops it.\",\n )\n s3_artifact_bucket: str = Field(\n \"\",\n description=(\n \"S3 bucket the retraining run writes the model artifact to (the same \"\n \"location the model-artifact init/sidecar reads). Empty => no output.\"\n ),\n )\n s3_artifact_prefix: str = Field(\n \"routeiq/routing-models\",\n description=\"S3 key prefix under the artifact bucket for the model output.\",\n )\n s3_input_uri: str = Field(\n \"\",\n description=\"Optional S3 URI of the training input dataset channel.\",\n )\n pipeline_name: str = Field(\n \"routeiq-routing-retrain\",\n description=\"SageMaker Pipeline name (pipeline mode).\",\n )\n schedule_expression: str = Field(\n \"rate(7 days)\",\n description=(\n \"EventBridge cron/rate expression for the retraining schedule rule \"\n \"(e.g. 'rate(7 days)' or 'cron(0 3 ? * SUN *)').\"\n ),\n )\n schedule_rule_name: str = Field(\n \"routeiq-routing-retrain\",\n description=\"EventBridge rule name for the retraining schedule.\",\n )\n job_name_prefix: str = Field(\n \"routeiq-routing-retrain\",\n description=\"Prefix for auto-generated SageMaker training-job names.\",\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H + Cluster M).\n\n Bundles the MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), shadow/mirror canary traffic (RouteIQ-4fd1), upstream\n LiteLLM router delegation (RouteIQ-8539), SageMaker registry/experiments\n (RouteIQ-93e9), and the offline eval harness (RouteIQ-8d24). All are\n independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``,\n ``ROUTEIQ_MLOPS__UPSTREAM_ROUTER__ENABLED``,\n ``ROUTEIQ_MLOPS__SAGEMAKER__ENABLED``,\n ``ROUTEIQ_MLOPS__OFFLINE_EVAL__ENABLED``,\n ``ROUTEIQ_MLOPS__FAILURE_CAPTURE__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n upstream_router: MLOpsUpstreamRouterSettings = Field(\n default_factory=MLOpsUpstreamRouterSettings, # type: ignore[arg-type]\n description=\"Upstream LiteLLM adaptive/quality/auto router delegation.\",\n )\n sagemaker: MLOpsSageMakerSettings = Field(\n default_factory=MLOpsSageMakerSettings, # type: ignore[arg-type]\n description=\"SageMaker Model Registry + Experiments integration.\",\n )\n offline_eval: MLOpsOfflineEvalSettings = Field(\n default_factory=MLOpsOfflineEvalSettings, # type: ignore[arg-type]\n description=\"Offline eval harness over a versioned golden dataset.\",\n )\n failure_capture: MLOpsFailureCaptureSettings = Field(\n default_factory=MLOpsFailureCaptureSettings, # type: ignore[arg-type]\n description=\"Low-rate failure/timeout-path eval sample capture.\",\n )\n retraining: MLOpsRetrainingSettings = Field(\n default_factory=MLOpsRetrainingSettings, # type: ignore[arg-type]\n description=\"SageMaker scheduled-retraining orchestration (RouteIQ-8a24).\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n include_marketplace_endpoints: bool = Field(\n False,\n description=(\n \"RouteIQ-7105: also enumerate Amazon Bedrock Marketplace / mantle \"\n \"custom-deployment endpoints (``ListMarketplaceModelEndpoints``) and \"\n \"register each REGISTERED endpoint ARN as a ``model_list`` arm \"\n \"(``bedrock/<endpointArn>``). This extends the serverless-FM scan to \"\n \"cover operator-deployed Marketplace models (the bedrock-mantle \"\n \"PrivateLink custom endpoints). Default OFF -- byte-stable: the extra \"\n \"control call is opt-in and adds no arms until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__INCLUDE_MARKETPLACE_ENDPOINTS``.\"\n ),\n )\n synthesis_mode: BedrockSynthesisMode = Field(\n BedrockSynthesisMode.DISTINCT,\n description=(\n \"RouteIQ-77e8: how discovered arms fold into ``model_list``. \"\n \"``distinct`` (default, byte-stable) keeps one ``model_name`` per \"\n \"arm; ``auto_group`` collapses every arm onto the single \"\n \"``auto_group_name`` group; ``logical_groups`` emits one group per \"\n \"LOGICAL model (e.g. ``anthropic.claude-sonnet-4``) fanned across its \"\n \"global/geo/regional/mantle arms (better observability than \"\n \"``auto_group``). When set this SUPERSEDES the legacy ``auto_group`` \"\n \"bool; ``auto_group=True`` with the default ``synthesis_mode`` is \"\n \"still honoured for back-compat (it maps to ``auto_group``). Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__SYNTHESIS_MODE``.\"\n ),\n )\n full_bedrock_coverage: bool = Field(\n False,\n description=(\n \"RouteIQ-77e8: rolled-up FULL-COVERAGE switch. When True, RouteIQ \"\n \"routes across the ENTIRE Bedrock + Bedrock-Marketplace (mantle) \"\n \"surface as one operator action -- it implies ``enabled=True``, \"\n \"``include_marketplace_endpoints=True``, and \"\n \"``synthesis_mode=logical_groups`` (unless ``synthesis_mode`` was set \"\n \"explicitly to a non-default value, which wins), with the \"\n \"global>geo>regional profile preference already on by default \"\n \"(``residency_constraint=False``). So the operator flips ONE flag \"\n \"instead of four to get intelligence-first routing over every \"\n \"serverless FM + registered marketplace endpoint in every source \"\n \"region. Default OFF -- byte-stable: nothing changes until enabled. \"\n \"Env: ``ROUTEIQ_BEDROCK_DISCOVERY__FULL_BEDROCK_COVERAGE``.\"\n ),\n )\n drift_metric_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-9a42: emit a model-catalogue DRIFT metric \"\n \"(``gateway.bedrock.catalogue_drift``) measuring \"\n \"models_available (discovered) vs models_configured (the live \"\n \"model_list) so a CloudWatch alarm can fire when the deployed \"\n \"catalogue drifts from what Bedrock actually offers. Default OFF -- \"\n \"byte-stable: no metric is recorded until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_METRIC_ENABLED``.\"\n ),\n )\n drift_check_interval_seconds: float = Field(\n 900.0,\n ge=30.0,\n le=86400.0,\n description=(\n \"RouteIQ-9a42: interval (seconds) between periodic leader-gated \"\n \"catalogue-drift checks. Only consulted when \"\n \"``drift_metric_enabled`` AND ``enabled`` are True; otherwise the \"\n \"periodic task never starts (byte-stable). Default 900s (15 min). \"\n \"Env: ``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_CHECK_INTERVAL_SECONDS``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n # ------------------------------------------------------------------\n # Effective-setting resolution (RouteIQ-77e8 full-coverage rollup)\n # ------------------------------------------------------------------\n # These compute the EFFECTIVE values a scan should use after applying the\n # ``full_bedrock_coverage`` rollup, WITHOUT mutating the stored fields (so\n # the persisted config stays exactly what the operator wrote). They are the\n # single source of truth for the rollup -- both ``merge_bedrock_discovered_models``\n # and any other caller resolve through them.\n\n @property\n def effective_enabled(self) -> bool:\n \"\"\"Discovery runs when explicitly enabled OR full-coverage is on.\"\"\"\n return bool(self.enabled or self.full_bedrock_coverage)\n\n @property\n def effective_include_marketplace_endpoints(self) -> bool:\n \"\"\"Full-coverage forces marketplace/mantle endpoints into the scan.\"\"\"\n return bool(self.include_marketplace_endpoints or self.full_bedrock_coverage)\n\n @property\n def effective_synthesis_mode(self) -> \"BedrockSynthesisMode\":\n \"\"\"Resolve the synthesis mode after rollup + legacy ``auto_group`` map.\n\n Precedence (highest first):\n 1. An explicit non-default ``synthesis_mode`` (operator chose it) wins\n outright -- even under full-coverage.\n 2. ``full_bedrock_coverage`` -> ``logical_groups`` (one group per\n logical model, the recommended full-coverage shape).\n 3. Legacy ``auto_group=True`` -> ``auto_group`` (back-compat: the old\n bool still means \"collapse onto one group\" when ``synthesis_mode``\n was left at its ``distinct`` default).\n 4. Otherwise the stored ``synthesis_mode`` (``distinct`` default).\n \"\"\"\n if self.synthesis_mode is not BedrockSynthesisMode.DISTINCT:\n return self.synthesis_mode\n if self.full_bedrock_coverage:\n return BedrockSynthesisMode.LOGICAL_GROUPS\n if self.auto_group:\n return BedrockSynthesisMode.AUTO_GROUP\n return BedrockSynthesisMode.DISTINCT\n\n\nclass BedrockRequestLeversSettings(BaseModel):\n \"\"\"Per-request Bedrock Converse levers (data plane), default-OFF.\n\n Drives :class:`litellm_llmrouter.gateway.plugins.bedrock_request_levers\n .BedrockRequestLeversPlugin`, which mutates the outbound request on the\n correct litellm seam (``CustomLogger.async_pre_call_deployment_hook``) so\n the mutations actually reach the Bedrock ``Converse`` call args.\n\n Three independently-gated levers (each a byte-stable no-op when off):\n\n - ``request_metadata`` (RouteIQ-294a): forward tenant identity\n (workspace / key) as Bedrock ``requestMetadata`` tags for cost\n attribution. Lands as the top-level ``requestMetadata`` completion kwarg,\n which litellm's converse transform maps from ``non_default_params``.\n - ``team_callbacks`` (RouteIQ-9cd8): set per-team logging sinks as the\n TOP-LEVEL request kwargs ``callbacks`` / ``success_callback`` /\n ``failure_callback`` (where litellm sources dynamic per-request\n callbacks), NOT ``metadata[callbacks]`` (never read). The global\n registration is unaffected.\n - ``cache_point`` (RouteIQ-b9ee): drive Bedrock prompt-caching via\n litellm's ``cache_control_injection_points`` mechanism (system-prefix\n message injection + tool_config cachePoint), NOT a bare ``tools[]``\n ``cachePoint`` entry (dropped by the converse transform).\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_BEDROCK_LEVERS__REQUEST_METADATA``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACKS``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_POINT``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_SYSTEM``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_TOOLS``,\n ``ROUTEIQ_BEDROCK_LEVERS__METADATA_PREFIX``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACK_MAP`` (JSON\n ``{\"team-a\": {\"success_callback\": [\"s3\"]}}``).\n \"\"\"\n\n request_metadata: bool = Field(\n False,\n description=(\n \"RouteIQ-294a: forward tenant identity (workspace_id / key_id) as \"\n \"Bedrock requestMetadata tags for cost attribution. Default OFF -- \"\n \"byte-stable: no requestMetadata kwarg is added until enabled.\"\n ),\n )\n metadata_prefix: str = Field(\n \"routeiq_\",\n description=(\n \"Key prefix for the injected requestMetadata tags (Bedrock keys must \"\n \"match [a-zA-Z0-9 :_@$#=/+,.-]). Only used when request_metadata=True.\"\n ),\n )\n team_callbacks: bool = Field(\n False,\n description=(\n \"RouteIQ-9cd8: set per-team logging sinks as TOP-LEVEL request kwargs \"\n \"callbacks/success_callback/failure_callback (the seam litellm reads \"\n \"dynamic per-request callbacks from). Default OFF; the global \"\n \"registration is untouched.\"\n ),\n )\n team_callback_map: dict[str, dict[str, list[str]]] = Field(\n default_factory=dict,\n description=(\n \"Map of team_id -> {success_callback|failure_callback|callbacks: \"\n '[sink names]}. e.g. {\"team-a\": {\"success_callback\": [\"s3\"]}}. Only '\n \"applied when team_callbacks=True and the request resolves to a \"\n \"mapped team.\"\n ),\n )\n cache_point: bool = Field(\n False,\n description=(\n \"RouteIQ-b9ee: enable Bedrock prompt-caching cachePoint injection via \"\n \"litellm cache_control_injection_points. Default OFF -- byte-stable.\"\n ),\n )\n cache_system: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a system-prefix cachePoint (message \"\n \"injection point, role=system). Default on.\"\n ),\n )\n cache_tools: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a tool_config cachePoint so the tool \"\n \"schema prefix is cached. Default on. Only takes effect when the \"\n \"request carries tools.\"\n ),\n )\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\nclass EngineMetricsSettings(BaseModel):\n \"\"\"Self-hosted-engine ``/metrics`` scrape configuration (default OFF).\n\n Drives :mod:`litellm_llmrouter.engine_metrics`, the credless scraper that\n GETs a self-hosted inference engine's Prometheus ``/metrics`` (vLLM, vLLM\n Production Stack, AIBrix, llm-d -- all serve the ``vllm:*`` family) and\n parses the queue-depth / KV-cache-pressure gauges\n (``vllm:num_requests_waiting``, ``vllm:kv_cache_usage_perc`` /\n ``vllm:gpu_cache_usage_perc``, ...). Those signals feed a *future*\n KV/queue-aware Layer-1 router or an autoscaler-into-the-engine.\n\n LAYERING: this reads the engine *frontend's* aggregate ``/metrics`` so\n Layer-1 can decide whether to send more load to this engine arm; it never\n scrapes individual worker pods (that would collapse the two-layer model --\n see ``docs/architecture/aws-rearchitecture/51-multinode-large-model-serving.md``\n Part 3).\n\n Default DISABLED (opt-in): with ``enabled=False`` the scraper is a byte-stable\n no-op -- importing the module does no I/O, and ``scrape()`` returns an empty\n ``reachable=False`` snapshot without touching the network. Live scraping is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_ENGINE_METRICS__ENABLED``,\n ``ROUTEIQ_ENGINE_METRICS__SCRAPE_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the self-hosted-engine /metrics scrape. Default OFF -- the \"\n \"scraper no-ops and does zero network I/O until an operator opts in \"\n \"(live scrape is operator-gated).\"\n ),\n )\n scrape_timeout: float = Field(\n 2.0,\n ge=0.1,\n le=30.0,\n description=(\n \"Per-scrape HTTP GET timeout in seconds. An engine that does not \"\n \"answer within this budget yields an empty (reachable=False) \"\n \"snapshot, never an exception.\"\n ),\n )\n kv_aware_routing_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-08d6/6a89: enable the KV-cache-/queue-aware routing path for \"\n \"self-hosted engine arms. When ON the router scrapes each candidate \"\n \"arm's engine /metrics (vllm:num_requests_waiting, kv_cache_usage_perc) \"\n \"and selects the LEAST-LOADED arm. Requires ``enabled`` (the scraper) \"\n \"to also be True. Default OFF -- byte-stable: the KV-aware pre-step is \"\n \"skipped entirely and routing is unchanged. Env: \"\n \"``ROUTEIQ_ENGINE_METRICS__KV_AWARE_ROUTING_ENABLED``.\"\n ),\n )\n kv_cache_usage_weight: float = Field(\n 1.0,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's KV-cache usage fraction (0..1) when \"\n \"scoring engine load for KV-aware routing. Higher => avoid \"\n \"cache-pressured arms more aggressively.\"\n ),\n )\n waiting_queue_weight: float = Field(\n 0.1,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's num_requests_waiting (queue depth) when \"\n \"scoring engine load for KV-aware routing. The load score is \"\n \"kv_cache_usage_weight*kv_usage + waiting_queue_weight*num_waiting.\"\n ),\n )\n\n\nclass MultinodeAffinitySettings(BaseModel):\n \"\"\"Multinode engine-affinity passthrough configuration (default OFF).\n\n Drives the LIVE wiring of :mod:`litellm_llmrouter.engine_affinity` +\n :mod:`litellm_llmrouter.conversation_affinity` into the request path via\n :class:`~litellm_llmrouter.gateway.plugin_callback_bridge.PluginCallbackBridge`\n (RouteIQ-bdd0 + RouteIQ-3316). When enabled, the bridge's deployment hook\n derives a session/conversation affinity key from the request, looks up a\n sticky decode-worker hint, and injects ``x-worker-instance-id`` /\n ``x-routeiq-affinity-key`` headers so a multi-turn conversation lands on the\n same disaggregated decode worker; the success hook records the\n ``response_id -> deployment`` mapping for the next turn.\n\n Default DISABLED (opt-in). With ``enabled=False`` the bridge's affinity logic\n is a byte-stable no-op: no tracker lookups, no header injection, no recording.\n\n The master enable flag is the EXISTING ``ROUTEIQ_MULTINODE_AFFINITY_ENABLED``\n env var, read by :func:`engine_affinity.multinode_affinity_enabled` (the\n single source of truth for the gate); this nested model adds only the\n disaggregation-default sub-flag.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MULTINODE_AFFINITY__DISAGG_DEFAULT_REMOTE_DECODE``,\n ``ROUTEIQ_MULTINODE_AFFINITY__DISAGG_DEFAULT_REMOTE_PREFILL``.\n \"\"\"\n\n disagg_default_remote_decode: bool = Field(\n False,\n description=(\n \"RouteIQ-3316: when set, every request (under the master \"\n \"ROUTEIQ_MULTINODE_AFFINITY_ENABLED gate) carries do_remote_decode=true \"\n \"to the engine as a forward-compat disaggregation-coordination \"\n \"passthrough. RouteIQ makes NO disaggregation decision -- it only \"\n \"carries the signal. Default OFF (byte-stable: non-disagg requests add \"\n \"zero params). A per-request metadata flag overrides this default.\"\n ),\n )\n disagg_default_remote_prefill: bool = Field(\n False,\n description=(\n \"RouteIQ-3316: when set, every request (under the master \"\n \"ROUTEIQ_MULTINODE_AFFINITY_ENABLED gate) carries do_remote_prefill=true \"\n \"to the engine as a forward-compat disaggregation-coordination \"\n \"passthrough. Default OFF (byte-stable). A per-request metadata flag \"\n \"overrides this default.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n # Per-workspace cross-account arm synthesis (RouteIQ-c6e9; default off).\n workspace_arm_synthesis: WorkspaceArmSynthesisSettings = Field(\n default_factory=WorkspaceArmSynthesisSettings, # type: ignore[arg-type]\n description=(\n \"Per-workspace cross-account Bedrock arm synthesis settings \"\n \"(RouteIQ-c6e9; default off / byte-stable).\"\n ),\n )\n # AWS Secrets Manager credential vault (RouteIQ-3d33; default off).\n secrets_vault: SecretsVaultSettings = Field(\n default_factory=SecretsVaultSettings, # type: ignore[arg-type]\n description=(\n \"AWS Secrets Manager credential vault settings (RouteIQ-3d33; \"\n \"default off / pass-through).\"\n ),\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n multinode_affinity: MultinodeAffinitySettings = Field(\n default_factory=MultinodeAffinitySettings, # type: ignore[arg-type]\n description=\"Multinode engine-affinity passthrough settings (RouteIQ-bdd0/3316).\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n api_surfaces: ApiSurfacesSettings = Field(\n default_factory=ApiSurfacesSettings, # type: ignore[arg-type]\n description=(\n \"First-class API surfaces, persisted spend, and self-service key \"\n \"settings (cluster N: RouteIQ-e48a / 67fe / 3215; all default on / \"\n \"byte-stable).\"\n ),\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n bedrock_levers: BedrockRequestLeversSettings = Field(\n default_factory=BedrockRequestLeversSettings, # type: ignore[arg-type]\n description=(\n \"Per-request Bedrock Converse levers (requestMetadata / team \"\n \"callbacks / cachePoint) on the pre-call mutation seam (default off).\"\n ),\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n region_routing: RegionRoutingSettings = Field(\n default_factory=RegionRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Per-request region-aware / data-residency candidate-filter settings \"\n \"(default off / identity).\"\n ),\n )\n capability_routing: CapabilityRoutingSettings = Field(\n default_factory=CapabilityRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Intelligence-first capability-tier floor candidate-filter settings \"\n \"(RouteIQ-8e37; default off / identity).\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n engine_metrics: EngineMetricsSettings = Field(\n default_factory=EngineMetricsSettings, # type: ignore[arg-type]\n description=(\n \"Self-hosted-engine /metrics scrape settings (vLLM / Production \"\n \"Stack / AIBrix / llm-d queue-depth + KV-cache gauges; default off).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_cluster_w2_aliases(cls, data: Any) -> Any:\n \"\"\"Map cluster-W2 flat env vars onto their nested settings fields.\n\n pydantic-settings won't resolve a flat top-level env var onto a nested\n ``BaseModel`` field, so (mirroring ``_map_flat_iam_aliases``) inject the\n flat values here, before validation. All DEFAULT OFF / unset, so the\n default path is byte-stable.\n\n * ``ROUTEIQ_GOVERNANCE_BACKEND`` (RouteIQ-a865) -> ``governance.backend``\n * ``ROUTEIQ_WORKSPACE_ARM_SYNTHESIS_ENABLED`` (RouteIQ-c6e9) ->\n ``workspace_arm_synthesis.enabled``\n * ``ROUTEIQ_SECRETS_VAULT_ENABLED`` (RouteIQ-3d33) ->\n ``secrets_vault.enabled``\n\n The nested ``ROUTEIQ_<SECTION>__<FIELD>`` form WINS if both are set.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n def _set_nested(section: str, field: str, value: Any) -> None:\n current = data.get(section)\n if current is None:\n data[section] = {field: value}\n elif isinstance(current, dict):\n current.setdefault(field, value)\n elif isinstance(current, BaseModel):\n # Only fill when the field is still at its falsy default.\n if not getattr(current, field, None):\n data[section] = current.model_copy(update={field: value})\n\n backend = _os.environ.get(\"ROUTEIQ_GOVERNANCE_BACKEND\")\n if backend:\n _set_nested(\"governance\", \"backend\", backend.strip().lower())\n\n arm_enabled = _os.environ.get(\"ROUTEIQ_WORKSPACE_ARM_SYNTHESIS_ENABLED\")\n if arm_enabled is not None and _truthy(arm_enabled):\n _set_nested(\"workspace_arm_synthesis\", \"enabled\", True)\n\n vault_enabled = _os.environ.get(\"ROUTEIQ_SECRETS_VAULT_ENABLED\")\n if vault_enabled is not None and _truthy(vault_enabled):\n _set_nested(\"secrets_vault\", \"enabled\", True)\n\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "1fae60b64eddcd5f212b98ab86ba7d8c74e50aae": {"t": 1785769070.5318694, "n": 135010, "h": "3dba14986257e233", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass BedrockSynthesisMode(str, Enum):\n \"\"\"How Bedrock auto-discovery folds discovered arms into ``model_list``.\n\n - ``distinct`` — one distinct ``model_name`` per discovered arm (the\n byte-stable historical default; the operator-authored\n list is only extended, never collapsed).\n - ``auto_group`` — every discovered serverless arm (across ALL providers\n and tiers) collapses onto ONE shared ``model_name``\n (``auto_group_name``) so a single routing group fans\n out over the whole Bedrock surface.\n - ``logical_groups`` — one group PER LOGICAL model (e.g.\n ``anthropic.claude-sonnet-4``) fanned across its\n global/geo/regional/mantle arms; better observability\n than ``auto_group`` (which collapses everything to one\n name) while still letting the bandit cascade per group.\n \"\"\"\n\n DISTINCT = \"distinct\"\n AUTO_GROUP = \"auto_group\"\n LOGICAL_GROUPS = \"logical_groups\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n retry_consumer_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-3ff5: enable the mode-(a) cascade RETRY consumer -- a \"\n \"component that reads the ``routeiq_cascade`` ladder on the cheapest \"\n \"rung's response, scores the response confidence, and RE-ISSUES the \"\n \"request pinned to the NEXT (stronger) rung when confidence is below \"\n \"``confidence_threshold``, climbing up to ``max_rungs`` rungs. Default \"\n \"OFF -- byte-stable: with it off a single cheapest-rung completion is \"\n \"returned unchanged. Env: \"\n \"``ROUTEIQ_COST_CASCADE__RETRY_CONSUMER_ENABLED``.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass RegionRoutingSettings(BaseModel):\n \"\"\"Per-request region-aware / data-residency candidate-filter configuration\n (RouteIQ-60cc).\n\n Bedrock cross-region inference profiles are already used STATICALLY (each\n ``model_list`` arm pins ``litellm_params.aws_region_name`` / a geo-prefixed\n inference profile). This block adds a PER-REQUEST region preference + a hard\n residency constraint on top, applied as a PRE-SCORING candidate filter (the\n same ``filter_routable_candidates`` seam every RouteIQ strategy already calls,\n so it composes with gov-ban + cooldown and benefits EVERY strategy -- no new\n strategy subclass needed).\n\n The requested region is resolved per request, FIRST match wins:\n\n 1. The request **header** named by :attr:`region_header` (read\n case-insensitively from ``request_kwargs['headers']`` /\n ``metadata['headers']``) -- e.g. ``X-RouteIQ-Region: eu``.\n 2. An explicit ``region`` / ``residency_region`` key in\n ``request_kwargs`` / ``metadata`` (e.g. a governance-workspace region the\n auth layer stamped onto the request metadata).\n\n A request is \"constrained\" (HARD residency) when EITHER the resolved value\n came with a residency flag (``residency`` / ``data_residency`` truthy in the\n same source) OR :attr:`hard_residency_default` is True. A constrained request\n is NEVER routed out-of-region: if no in-region candidate exists the candidate\n set is emptied (the strategy then returns ``None`` / triggers a fallback) --\n a residency violation must never silently leak. An UNCONSTRAINED request\n PREFERS in-region but falls back to the full set when no in-region arm exists.\n\n A candidate's region is read from ``litellm_params.aws_region_name`` (the\n field every Bedrock arm already sets), then normalized through\n :attr:`region_map` so a request token (``\"eu\"``) can map to one or more\n concrete AWS regions (``[\"eu-west-1\", \"eu-central-1\"]``). When a request\n token is absent from ``region_map`` it is matched verbatim (so an\n ``aws_region_name``-equal request token works with zero map config).\n\n Env vars (``__`` nested delimiter): ``ROUTEIQ_REGION_ROUTING__ENABLED``,\n ``ROUTEIQ_REGION_ROUTING__REGION_HEADER``,\n ``ROUTEIQ_REGION_ROUTING__REGION_MAP`` (JSON map),\n ``ROUTEIQ_REGION_ROUTING__HARD_RESIDENCY_DEFAULT``.\n\n Default OFF / identity: when ``enabled`` is False the filter is a byte-stable\n no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request region-aware / data-residency candidate \"\n \"pre-filter. Default off -> identity (byte-stable no-op).\"\n ),\n )\n region_header: str = Field(\n \"X-RouteIQ-Region\",\n description=(\n \"Request header carrying the desired region/residency token (read \"\n \"case-insensitively). Checked before the request_kwargs/metadata \"\n \"region keys.\"\n ),\n )\n region_map: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request region TOKEN -> list of concrete AWS regions that \"\n 'satisfy it (e.g. {\"eu\": [\"eu-west-1\", \"eu-central-1\"]}). A token '\n \"absent from the map is matched verbatim against the candidate \"\n \"``aws_region_name``, so an exact-region token needs no map entry.\"\n ),\n )\n hard_residency_default: bool = Field(\n False,\n description=(\n \"When True, EVERY region-constrained request is treated as a HARD \"\n \"residency constraint (never routed out-of-region) even without a \"\n \"per-request residency flag. When False (default) only requests that \"\n \"carry an explicit residency flag are hard-constrained; an \"\n \"unflagged region is a soft preference (in-region first, full-set \"\n \"fallback).\"\n ),\n )\n\n\nclass CapabilityRoutingSettings(BaseModel):\n \"\"\"INTELLIGENCE-FIRST capability-tier floor (RouteIQ-8e37).\n\n Makes the RL router pick the most CAPABLE model for the task, not the\n cheapest, by enforcing a per-request capability-tier FLOOR as a PRE-SCORING\n candidate filter on the SAME ``filter_routable_candidates`` seam every\n RouteIQ strategy already calls (the region filter — RouteIQ-60cc — is the\n precedent). It composes with gov-ban + cooldown + region and benefits EVERY\n strategy (default / ML / bandit / centroid / FALLBACK) with no new strategy\n subclass and no per-path edit.\n\n Each model carries an ordered capability TIER — ``fast`` < ``advanced`` <\n ``expert`` (see :attr:`tier_order`). The request DIFFICULTY (resolved from\n the warm centroid complexity classifier + reasoning markers — never a cold\n model load on the hot path) maps to a REQUIRED minimum tier: a simple task\n accepts any tier; a complex / reasoning task requires ``expert``. When\n gating is :attr:`enabled` and the request needs ``>=`` a tier, models BELOW\n that tier are DROPPED from the candidate set BEFORE scoring, so a reasoning\n task NEVER scores a sub-tier model — even in fallback / exploration.\n\n Tiers are CONFIG-DRIVEN: :attr:`model_tiers` overrides the per-family\n defaults (:data:`litellm_llmrouter.centroid_routing.MODEL_CAPABILITY_TIERS`).\n An UNKNOWN model defaults to :attr:`default_tier` (``advanced``, a safe\n MIDDLE tier) so it is never WRONGLY excluded by a tier floor.\n\n Capability is a PREFERENCE floor, not a hard residency deny: in the default\n SOFT mode, if the floor would EMPTY the candidate set (no capable model\n available) it DEGRADES GRACEFULLY — logs and falls back to the full set\n rather than returning None / 500. Set :attr:`strict` to fail-closed-to-empty\n instead (the strategy then yields None / triggers a fallback).\n\n Env vars (``__`` nested delimiter):\n ``ROUTEIQ_CAPABILITY_ROUTING__ENABLED``,\n ``ROUTEIQ_CAPABILITY_ROUTING__STRICT``,\n ``ROUTEIQ_CAPABILITY_ROUTING__DEFAULT_TIER``,\n ``ROUTEIQ_CAPABILITY_ROUTING__MODEL_TIERS`` (JSON map),\n ``ROUTEIQ_CAPABILITY_ROUTING__COMPLEX_MIN_TIER``,\n ``ROUTEIQ_CAPABILITY_ROUTING__SIMPLE_MIN_TIER``.\n\n Default OFF / identity: when :attr:`enabled` is False the filter is a\n byte-stable no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request capability-tier FLOOR candidate pre-filter \"\n \"(intelligence-first routing). Default off -> identity (byte-stable \"\n \"no-op).\"\n ),\n )\n strict: bool = Field(\n False,\n description=(\n \"When True, the capability floor is HARD: if no candidate meets the \"\n \"required tier the set is EMPTIED (strategy yields None / fallback). \"\n \"When False (default) the floor DEGRADES GRACEFULLY — it logs and \"\n \"returns the full set rather than emptying it (capability is a \"\n \"PREFERENCE floor, not a residency deny; never 500 on an empty set).\"\n ),\n )\n tier_order: List[str] = Field(\n default_factory=lambda: [\"fast\", \"advanced\", \"expert\"],\n description=(\n \"Ordered capability tiers from least to most capable. A model's tier \"\n \"is compared by its INDEX here; a required tier of ``expert`` drops \"\n \"every model whose tier index is below ``expert``'s. Operators may \"\n \"extend / reorder this for custom fleets.\"\n ),\n )\n default_tier: str = Field(\n \"advanced\",\n description=(\n \"Tier assigned to a model with NO known / configured tier. A SAFE \"\n \"MIDDLE tier (``advanced``) so an unknown model is never WRONGLY \"\n \"excluded by a tier floor (it clears a ``fast``/``advanced`` floor \"\n \"and is only dropped by an ``expert`` floor).\"\n ),\n )\n model_tiers: Dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Operator override map of model id (or family substring) -> tier \"\n 'name (e.g. {\"my-custom-7b\": \"fast\", \"my-frontier\": \"expert\"}). '\n \"Checked BEFORE the per-family defaults so an operator can pin any \"\n \"model's tier. A tier value not in ``tier_order`` falls back to \"\n \"``default_tier``.\"\n ),\n )\n complex_min_tier: str = Field(\n \"expert\",\n description=(\n \"Required MINIMUM tier for a COMPLEX / reasoning-difficulty request \"\n \"(centroid 'complex' tier OR reasoning markers detected). Default \"\n \"``expert`` — a hard-reasoning task never scores a sub-expert model.\"\n ),\n )\n simple_min_tier: str = Field(\n \"fast\",\n description=(\n \"Required MINIMUM tier for a SIMPLE-difficulty request. Default \"\n \"``fast`` — a simple task accepts ANY tier (the lowest floor).\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n access_group_enforcement: bool = Field(\n False,\n description=(\n \"RouteIQ-2fa1: enforce per-key/per-tool MCP access groups on the LIVE \"\n \"invocation path. When True, invoking a tool whose backing server \"\n \"declares ``metadata['access_groups']`` requires the caller to present \"\n \"at least one matching access group; a non-member call is REJECTED at \"\n \"``invoke_tool`` (the terminal path, before any outbound MCP call). \"\n \"When False (default) the access-group metadata is purely advisory \"\n \"(used only by the discovery/registry listing filter) -- byte-stable. \"\n \"Servers with NO ``access_groups`` configured are unrestricted on \"\n \"either setting (open by default). Env: \"\n \"``ROUTEIQ_MCP__ACCESS_GROUP_ENFORCEMENT``.\"\n ),\n )\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n agentcore_memory_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-60e7: persist A2A agent/conversation state into AWS Bedrock \"\n \"AgentCore Memory after a successful agent invocation (via the live \"\n \"bedrock-agentcore-mcp plugin's CreateEvent). Default OFF -- \"\n \"byte-stable: the A2A invocation path is unchanged and issues no \"\n \"AgentCore call until an operator opts in. Env: \"\n \"``ROUTEIQ_A2A__AGENTCORE_MEMORY_ENABLED`` (or A2A_AGENTCORE_MEMORY_ENABLED).\"\n ),\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass ApiSurfacesSettings(BaseModel):\n \"\"\"First-class API surfaces, persisted spend, and self-service keys\n (RouteIQ cluster N: e48a / 67fe / 3215).\n\n All flags DEFAULT to the byte-stable behaviour:\n\n * ``bare_paths_enabled`` -- register the bare-path delegators (Responses,\n rerank, RAG, batches, vector-store, audio, images) on the parent app so\n they get RouteIQ middleware + documentation (default ON; a no-op when\n LiteLLM is not mounted).\n * ``persisted_spend_enabled`` -- aggregate the spend report from the durable\n ``governance_spend`` rollup (default ON; degrades to the zero/no-op report\n automatically when no ``DATABASE_URL`` is configured).\n * ``self_service_keys_enabled`` -- expose the user-tier self-service key\n endpoints (default ON; they are scope-isolated to the caller's identity).\n\n Env vars: ``ROUTEIQ_API_SURFACES__BARE_PATHS_ENABLED``,\n ``ROUTEIQ_API_SURFACES__PERSISTED_SPEND_ENABLED``,\n ``ROUTEIQ_API_SURFACES__SELF_SERVICE_KEYS_ENABLED``.\n \"\"\"\n\n bare_paths_enabled: bool = Field(\n True,\n description=(\n \"Register first-class bare-path API surface delegators so they get \"\n \"RouteIQ middleware + OpenAPI documentation (no-op without the /v1 \"\n \"LiteLLM mount).\"\n ),\n )\n persisted_spend_enabled: bool = Field(\n True,\n description=(\n \"Aggregate the spend report from the durable governance_spend \"\n \"rollup (degrades to a zero report when no DATABASE_URL is set).\"\n ),\n )\n self_service_keys_enabled: bool = Field(\n True,\n description=(\n \"Expose user-tier self-service key endpoints (scope-isolated to the \"\n \"caller's identity).\"\n ),\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n reward_profile: str = Field(\n \"balanced\",\n description=(\n \"INTELLIGENCE-FIRST reward profile (RouteIQ-8e37). A one-flag switch \"\n \"for the bandit's reward weights:\\n\"\n \" - ``balanced`` (DEFAULT, byte-stable): the explicit \"\n \"``w_quality`` / ``w_cost`` / ``w_latency`` below (0.5 / 0.4 / 0.1).\\n\"\n \" - ``intelligence_first`` (RECOMMENDED for the Claude-Code recipe): \"\n \"weight quality ~1.0, cost ~0, latency ~0 — within the capable tier \"\n \"the bandit LEARNS per-bucket which model is actually best from the \"\n \"LLM-judge (Opus vs MiniMax vs Kimi vs DeepSeek), not the cheapest.\\n\"\n \" - ``cost_aware``: weight cost more heavily (0.2 / 0.7 / 0.1).\\n\"\n \"Any non-default profile OVERRIDES the explicit ``w_*`` weights; \"\n \"``balanced`` leaves them untouched so existing configs are unchanged. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__REWARD_PROFILE``.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n\n def resolved_reward_weights(self) -> tuple[float, float, float]:\n \"\"\"Resolve ``(w_quality, w_cost, w_latency)`` for the active profile.\n\n ``balanced`` (default) returns the explicit ``w_quality`` / ``w_cost`` /\n ``w_latency`` UNCHANGED (byte-stable). ``intelligence_first`` and\n ``cost_aware`` are one-flag presets that OVERRIDE those weights. An\n unknown profile falls back to the explicit weights (treated as balanced)\n so a typo never silently zeroes a weight.\n \"\"\"\n profile = (self.reward_profile or \"balanced\").strip().lower()\n if profile == \"intelligence_first\":\n return (1.0, 0.0, 0.0)\n if profile == \"cost_aware\":\n return (0.2, 0.7, 0.1)\n # balanced (default) or unknown -> the explicit per-weight fields.\n return (self.w_quality, self.w_cost, self.w_latency)\n\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsFailureCaptureSettings(BaseModel):\n \"\"\"Low-rate capture from the eval COLLECT FAILURE/timeout path (RouteIQ-d365).\n\n The eval COLLECT arm only samples SUCCESS responses, so AGGREGATE/FEEDBACK\n never sees which models/strategies are error-prone. When enabled, a\n parallel, clearly-labeled, low-rate capture fires on the FAILURE path\n (``async_log_failure_event``) so the loop can downweight error-prone\n models/strategies.\n\n Captured failure samples are tagged (``outcome=\"failure\"`` +\n ``error_type``) so AGGREGATE never confuses them with successful responses.\n Default OFF and sampled at a LOW rate so a failure storm cannot flood the\n eval queue.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable low-rate capture of FAILURE/timeout-path eval samples.\",\n )\n sample_rate: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Fraction of failure events to capture (kept LOW so a failure storm \"\n \"does not flood the eval queue).\"\n ),\n )\n\n\nclass MLOpsUpstreamRouterSettings(BaseModel):\n \"\"\"Delegate routing to an upstream LiteLLM adaptive/quality/auto router\n (RouteIQ-8539).\n\n Upstream LiteLLM ships its OWN online-bandit / quality-feedback routing\n modes (``litellm.router_strategy.adaptive_router`` /\n ``quality_router`` / ``auto_router``), each configured per-deployment via the\n ``auto_router/...`` model prefix and holding an in-memory\n ``AdaptiveRouterUpdateQueue``. This block lets RouteIQ DELEGATE to one of\n those modes (alongside the RouteIQ ML strategies) and FLUSH the chosen\n router's update queue to the durable store on the MLOps cadence.\n\n Default OFF: with ``enabled=False`` RouteIQ never touches the upstream\n router registries, so this is a byte-stable no-op until an operator opts in.\n The upstream router itself must additionally be configured in the LiteLLM\n ``model_list`` (the ``auto_router/...`` deployment) -- this flag only wires\n RouteIQ's delegation + queue-flush, it does not create the deployment.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Delegate to an upstream LiteLLM adaptive/quality/auto router and \"\n \"flush its update queue to the durable store.\"\n ),\n )\n mode: str = Field(\n \"adaptive\",\n description=(\n \"Which upstream router family to delegate to: 'adaptive' \"\n \"(adaptive_router), 'quality' (quality_router), or 'auto' \"\n \"(auto_router). Selects which of the LiteLLM Router's per-name \"\n \"router dicts RouteIQ delegates to / flushes.\"\n ),\n )\n router_name: str = Field(\n \"\",\n description=(\n \"Optional specific upstream router_name (the model_list deployment \"\n \"name) to delegate to. Empty => the first registered router of the \"\n \"selected mode.\"\n ),\n )\n flush_queue: bool = Field(\n True,\n description=(\n \"On each MLOps feedback cadence, drain the selected upstream \"\n \"router's AdaptiveRouterUpdateQueue (state + session) to the durable \"\n \"store (the proxy prisma client). No-op when the mode has no queue \"\n \"(auto/quality) or no durable client is available.\"\n ),\n )\n\n\nclass MLOpsSageMakerSettings(BaseModel):\n \"\"\"SageMaker Model Registry + Experiments integration (RouteIQ-93e9).\n\n Registers a trained routing-model artifact into a SageMaker Model Package\n Group (with an approval status) and logs run metrics to SageMaker\n Experiments. Cred-free + mockable: the adapter takes an injected boto3\n sagemaker client, so unit tests pass a mock and never touch AWS.\n\n Default OFF: live SageMaker calls are operator-gated. With\n ``enabled=False`` the adapter's register/log methods short-circuit and never\n construct a boto3 client.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable SageMaker Model Registry + Experiments integration. Live \"\n \"SageMaker calls are operator-gated; default off.\"\n ),\n )\n model_package_group_name: str = Field(\n \"routeiq-routing-models\",\n description=\"SageMaker Model Package Group the routing artifact registers into.\",\n )\n approval_status: str = Field(\n \"PendingManualApproval\",\n description=(\n \"ModelApprovalStatus for the registered package \"\n \"(PendingManualApproval | Approved | Rejected).\"\n ),\n )\n experiment_name: str = Field(\n \"routeiq-routing-eval\",\n description=\"SageMaker Experiment name run metrics are logged under.\",\n )\n region: str = Field(\n \"\",\n description=\"AWS region for the boto3 sagemaker client (empty => default chain).\",\n )\n\n\nclass MLOpsOfflineEvalSettings(BaseModel):\n \"\"\"Offline eval harness over a versioned golden dataset (RouteIQ-8d24).\n\n Replays a versioned golden dataset (request -> expected-quality) through the\n eval judge / a strategy and reports per-strategy quality OFFLINE -- no live\n traffic. Default OFF; the harness is also invokable directly (tests / a CLI\n / a batch job) without flipping this flag.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the offline eval harness (also directly invokable).\",\n )\n golden_dataset_path: str = Field(\n \"\",\n description=(\n \"Path to the versioned golden dataset JSON. Empty => the bundled \"\n \"default fixture under config/golden_eval/.\"\n ),\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H + Cluster M).\n\n Bundles the MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), shadow/mirror canary traffic (RouteIQ-4fd1), upstream\n LiteLLM router delegation (RouteIQ-8539), SageMaker registry/experiments\n (RouteIQ-93e9), and the offline eval harness (RouteIQ-8d24). All are\n independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``,\n ``ROUTEIQ_MLOPS__UPSTREAM_ROUTER__ENABLED``,\n ``ROUTEIQ_MLOPS__SAGEMAKER__ENABLED``,\n ``ROUTEIQ_MLOPS__OFFLINE_EVAL__ENABLED``,\n ``ROUTEIQ_MLOPS__FAILURE_CAPTURE__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n upstream_router: MLOpsUpstreamRouterSettings = Field(\n default_factory=MLOpsUpstreamRouterSettings, # type: ignore[arg-type]\n description=\"Upstream LiteLLM adaptive/quality/auto router delegation.\",\n )\n sagemaker: MLOpsSageMakerSettings = Field(\n default_factory=MLOpsSageMakerSettings, # type: ignore[arg-type]\n description=\"SageMaker Model Registry + Experiments integration.\",\n )\n offline_eval: MLOpsOfflineEvalSettings = Field(\n default_factory=MLOpsOfflineEvalSettings, # type: ignore[arg-type]\n description=\"Offline eval harness over a versioned golden dataset.\",\n )\n failure_capture: MLOpsFailureCaptureSettings = Field(\n default_factory=MLOpsFailureCaptureSettings, # type: ignore[arg-type]\n description=\"Low-rate failure/timeout-path eval sample capture.\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n include_marketplace_endpoints: bool = Field(\n False,\n description=(\n \"RouteIQ-7105: also enumerate Amazon Bedrock Marketplace / mantle \"\n \"custom-deployment endpoints (``ListMarketplaceModelEndpoints``) and \"\n \"register each REGISTERED endpoint ARN as a ``model_list`` arm \"\n \"(``bedrock/<endpointArn>``). This extends the serverless-FM scan to \"\n \"cover operator-deployed Marketplace models (the bedrock-mantle \"\n \"PrivateLink custom endpoints). Default OFF -- byte-stable: the extra \"\n \"control call is opt-in and adds no arms until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__INCLUDE_MARKETPLACE_ENDPOINTS``.\"\n ),\n )\n synthesis_mode: BedrockSynthesisMode = Field(\n BedrockSynthesisMode.DISTINCT,\n description=(\n \"RouteIQ-77e8: how discovered arms fold into ``model_list``. \"\n \"``distinct`` (default, byte-stable) keeps one ``model_name`` per \"\n \"arm; ``auto_group`` collapses every arm onto the single \"\n \"``auto_group_name`` group; ``logical_groups`` emits one group per \"\n \"LOGICAL model (e.g. ``anthropic.claude-sonnet-4``) fanned across its \"\n \"global/geo/regional/mantle arms (better observability than \"\n \"``auto_group``). When set this SUPERSEDES the legacy ``auto_group`` \"\n \"bool; ``auto_group=True`` with the default ``synthesis_mode`` is \"\n \"still honoured for back-compat (it maps to ``auto_group``). Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__SYNTHESIS_MODE``.\"\n ),\n )\n full_bedrock_coverage: bool = Field(\n False,\n description=(\n \"RouteIQ-77e8: rolled-up FULL-COVERAGE switch. When True, RouteIQ \"\n \"routes across the ENTIRE Bedrock + Bedrock-Marketplace (mantle) \"\n \"surface as one operator action -- it implies ``enabled=True``, \"\n \"``include_marketplace_endpoints=True``, and \"\n \"``synthesis_mode=logical_groups`` (unless ``synthesis_mode`` was set \"\n \"explicitly to a non-default value, which wins), with the \"\n \"global>geo>regional profile preference already on by default \"\n \"(``residency_constraint=False``). So the operator flips ONE flag \"\n \"instead of four to get intelligence-first routing over every \"\n \"serverless FM + registered marketplace endpoint in every source \"\n \"region. Default OFF -- byte-stable: nothing changes until enabled. \"\n \"Env: ``ROUTEIQ_BEDROCK_DISCOVERY__FULL_BEDROCK_COVERAGE``.\"\n ),\n )\n drift_metric_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-9a42: emit a model-catalogue DRIFT metric \"\n \"(``gateway.bedrock.catalogue_drift``) measuring \"\n \"models_available (discovered) vs models_configured (the live \"\n \"model_list) so a CloudWatch alarm can fire when the deployed \"\n \"catalogue drifts from what Bedrock actually offers. Default OFF -- \"\n \"byte-stable: no metric is recorded until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_METRIC_ENABLED``.\"\n ),\n )\n drift_check_interval_seconds: float = Field(\n 900.0,\n ge=30.0,\n le=86400.0,\n description=(\n \"RouteIQ-9a42: interval (seconds) between periodic leader-gated \"\n \"catalogue-drift checks. Only consulted when \"\n \"``drift_metric_enabled`` AND ``enabled`` are True; otherwise the \"\n \"periodic task never starts (byte-stable). Default 900s (15 min). \"\n \"Env: ``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_CHECK_INTERVAL_SECONDS``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n # ------------------------------------------------------------------\n # Effective-setting resolution (RouteIQ-77e8 full-coverage rollup)\n # ------------------------------------------------------------------\n # These compute the EFFECTIVE values a scan should use after applying the\n # ``full_bedrock_coverage`` rollup, WITHOUT mutating the stored fields (so\n # the persisted config stays exactly what the operator wrote). They are the\n # single source of truth for the rollup -- both ``merge_bedrock_discovered_models``\n # and any other caller resolve through them.\n\n @property\n def effective_enabled(self) -> bool:\n \"\"\"Discovery runs when explicitly enabled OR full-coverage is on.\"\"\"\n return bool(self.enabled or self.full_bedrock_coverage)\n\n @property\n def effective_include_marketplace_endpoints(self) -> bool:\n \"\"\"Full-coverage forces marketplace/mantle endpoints into the scan.\"\"\"\n return bool(self.include_marketplace_endpoints or self.full_bedrock_coverage)\n\n @property\n def effective_synthesis_mode(self) -> \"BedrockSynthesisMode\":\n \"\"\"Resolve the synthesis mode after rollup + legacy ``auto_group`` map.\n\n Precedence (highest first):\n 1. An explicit non-default ``synthesis_mode`` (operator chose it) wins\n outright -- even under full-coverage.\n 2. ``full_bedrock_coverage`` -> ``logical_groups`` (one group per\n logical model, the recommended full-coverage shape).\n 3. Legacy ``auto_group=True`` -> ``auto_group`` (back-compat: the old\n bool still means \"collapse onto one group\" when ``synthesis_mode``\n was left at its ``distinct`` default).\n 4. Otherwise the stored ``synthesis_mode`` (``distinct`` default).\n \"\"\"\n if self.synthesis_mode is not BedrockSynthesisMode.DISTINCT:\n return self.synthesis_mode\n if self.full_bedrock_coverage:\n return BedrockSynthesisMode.LOGICAL_GROUPS\n if self.auto_group:\n return BedrockSynthesisMode.AUTO_GROUP\n return BedrockSynthesisMode.DISTINCT\n\n\nclass BedrockRequestLeversSettings(BaseModel):\n \"\"\"Per-request Bedrock Converse levers (data plane), default-OFF.\n\n Drives :class:`litellm_llmrouter.gateway.plugins.bedrock_request_levers\n .BedrockRequestLeversPlugin`, which mutates the outbound request on the\n correct litellm seam (``CustomLogger.async_pre_call_deployment_hook``) so\n the mutations actually reach the Bedrock ``Converse`` call args.\n\n Three independently-gated levers (each a byte-stable no-op when off):\n\n - ``request_metadata`` (RouteIQ-294a): forward tenant identity\n (workspace / key) as Bedrock ``requestMetadata`` tags for cost\n attribution. Lands as the top-level ``requestMetadata`` completion kwarg,\n which litellm's converse transform maps from ``non_default_params``.\n - ``team_callbacks`` (RouteIQ-9cd8): set per-team logging sinks as the\n TOP-LEVEL request kwargs ``callbacks`` / ``success_callback`` /\n ``failure_callback`` (where litellm sources dynamic per-request\n callbacks), NOT ``metadata[callbacks]`` (never read). The global\n registration is unaffected.\n - ``cache_point`` (RouteIQ-b9ee): drive Bedrock prompt-caching via\n litellm's ``cache_control_injection_points`` mechanism (system-prefix\n message injection + tool_config cachePoint), NOT a bare ``tools[]``\n ``cachePoint`` entry (dropped by the converse transform).\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_BEDROCK_LEVERS__REQUEST_METADATA``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACKS``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_POINT``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_SYSTEM``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_TOOLS``,\n ``ROUTEIQ_BEDROCK_LEVERS__METADATA_PREFIX``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACK_MAP`` (JSON\n ``{\"team-a\": {\"success_callback\": [\"s3\"]}}``).\n \"\"\"\n\n request_metadata: bool = Field(\n False,\n description=(\n \"RouteIQ-294a: forward tenant identity (workspace_id / key_id) as \"\n \"Bedrock requestMetadata tags for cost attribution. Default OFF -- \"\n \"byte-stable: no requestMetadata kwarg is added until enabled.\"\n ),\n )\n metadata_prefix: str = Field(\n \"routeiq_\",\n description=(\n \"Key prefix for the injected requestMetadata tags (Bedrock keys must \"\n \"match [a-zA-Z0-9 :_@$#=/+,.-]). Only used when request_metadata=True.\"\n ),\n )\n team_callbacks: bool = Field(\n False,\n description=(\n \"RouteIQ-9cd8: set per-team logging sinks as TOP-LEVEL request kwargs \"\n \"callbacks/success_callback/failure_callback (the seam litellm reads \"\n \"dynamic per-request callbacks from). Default OFF; the global \"\n \"registration is untouched.\"\n ),\n )\n team_callback_map: dict[str, dict[str, list[str]]] = Field(\n default_factory=dict,\n description=(\n \"Map of team_id -> {success_callback|failure_callback|callbacks: \"\n '[sink names]}. e.g. {\"team-a\": {\"success_callback\": [\"s3\"]}}. Only '\n \"applied when team_callbacks=True and the request resolves to a \"\n \"mapped team.\"\n ),\n )\n cache_point: bool = Field(\n False,\n description=(\n \"RouteIQ-b9ee: enable Bedrock prompt-caching cachePoint injection via \"\n \"litellm cache_control_injection_points. Default OFF -- byte-stable.\"\n ),\n )\n cache_system: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a system-prefix cachePoint (message \"\n \"injection point, role=system). Default on.\"\n ),\n )\n cache_tools: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a tool_config cachePoint so the tool \"\n \"schema prefix is cached. Default on. Only takes effect when the \"\n \"request carries tools.\"\n ),\n )\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\nclass EngineMetricsSettings(BaseModel):\n \"\"\"Self-hosted-engine ``/metrics`` scrape configuration (default OFF).\n\n Drives :mod:`litellm_llmrouter.engine_metrics`, the credless scraper that\n GETs a self-hosted inference engine's Prometheus ``/metrics`` (vLLM, vLLM\n Production Stack, AIBrix, llm-d -- all serve the ``vllm:*`` family) and\n parses the queue-depth / KV-cache-pressure gauges\n (``vllm:num_requests_waiting``, ``vllm:kv_cache_usage_perc`` /\n ``vllm:gpu_cache_usage_perc``, ...). Those signals feed a *future*\n KV/queue-aware Layer-1 router or an autoscaler-into-the-engine.\n\n LAYERING: this reads the engine *frontend's* aggregate ``/metrics`` so\n Layer-1 can decide whether to send more load to this engine arm; it never\n scrapes individual worker pods (that would collapse the two-layer model --\n see ``docs/architecture/aws-rearchitecture/51-multinode-large-model-serving.md``\n Part 3).\n\n Default DISABLED (opt-in): with ``enabled=False`` the scraper is a byte-stable\n no-op -- importing the module does no I/O, and ``scrape()`` returns an empty\n ``reachable=False`` snapshot without touching the network. Live scraping is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_ENGINE_METRICS__ENABLED``,\n ``ROUTEIQ_ENGINE_METRICS__SCRAPE_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the self-hosted-engine /metrics scrape. Default OFF -- the \"\n \"scraper no-ops and does zero network I/O until an operator opts in \"\n \"(live scrape is operator-gated).\"\n ),\n )\n scrape_timeout: float = Field(\n 2.0,\n ge=0.1,\n le=30.0,\n description=(\n \"Per-scrape HTTP GET timeout in seconds. An engine that does not \"\n \"answer within this budget yields an empty (reachable=False) \"\n \"snapshot, never an exception.\"\n ),\n )\n kv_aware_routing_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-08d6/6a89: enable the KV-cache-/queue-aware routing path for \"\n \"self-hosted engine arms. When ON the router scrapes each candidate \"\n \"arm's engine /metrics (vllm:num_requests_waiting, kv_cache_usage_perc) \"\n \"and selects the LEAST-LOADED arm. Requires ``enabled`` (the scraper) \"\n \"to also be True. Default OFF -- byte-stable: the KV-aware pre-step is \"\n \"skipped entirely and routing is unchanged. Env: \"\n \"``ROUTEIQ_ENGINE_METRICS__KV_AWARE_ROUTING_ENABLED``.\"\n ),\n )\n kv_cache_usage_weight: float = Field(\n 1.0,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's KV-cache usage fraction (0..1) when \"\n \"scoring engine load for KV-aware routing. Higher => avoid \"\n \"cache-pressured arms more aggressively.\"\n ),\n )\n waiting_queue_weight: float = Field(\n 0.1,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's num_requests_waiting (queue depth) when \"\n \"scoring engine load for KV-aware routing. The load score is \"\n \"kv_cache_usage_weight*kv_usage + waiting_queue_weight*num_waiting.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n api_surfaces: ApiSurfacesSettings = Field(\n default_factory=ApiSurfacesSettings, # type: ignore[arg-type]\n description=(\n \"First-class API surfaces, persisted spend, and self-service key \"\n \"settings (cluster N: RouteIQ-e48a / 67fe / 3215; all default on / \"\n \"byte-stable).\"\n ),\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n bedrock_levers: BedrockRequestLeversSettings = Field(\n default_factory=BedrockRequestLeversSettings, # type: ignore[arg-type]\n description=(\n \"Per-request Bedrock Converse levers (requestMetadata / team \"\n \"callbacks / cachePoint) on the pre-call mutation seam (default off).\"\n ),\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n region_routing: RegionRoutingSettings = Field(\n default_factory=RegionRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Per-request region-aware / data-residency candidate-filter settings \"\n \"(default off / identity).\"\n ),\n )\n capability_routing: CapabilityRoutingSettings = Field(\n default_factory=CapabilityRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Intelligence-first capability-tier floor candidate-filter settings \"\n \"(RouteIQ-8e37; default off / identity).\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n engine_metrics: EngineMetricsSettings = Field(\n default_factory=EngineMetricsSettings, # type: ignore[arg-type]\n description=(\n \"Self-hosted-engine /metrics scrape settings (vLLM / Production \"\n \"Stack / AIBrix / llm-d queue-depth + KV-cache gauges; default off).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "2c19920b1379fd66353ed977659b3e077a3ddd63": {"t": 1785769072.1706488, "n": 122581, "h": "cb420005d5f6676b", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n retry_consumer_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-3ff5: enable the mode-(a) cascade RETRY consumer -- a \"\n \"component that reads the ``routeiq_cascade`` ladder on the cheapest \"\n \"rung's response, scores the response confidence, and RE-ISSUES the \"\n \"request pinned to the NEXT (stronger) rung when confidence is below \"\n \"``confidence_threshold``, climbing up to ``max_rungs`` rungs. Default \"\n \"OFF -- byte-stable: with it off a single cheapest-rung completion is \"\n \"returned unchanged. Env: \"\n \"``ROUTEIQ_COST_CASCADE__RETRY_CONSUMER_ENABLED``.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass RegionRoutingSettings(BaseModel):\n \"\"\"Per-request region-aware / data-residency candidate-filter configuration\n (RouteIQ-60cc).\n\n Bedrock cross-region inference profiles are already used STATICALLY (each\n ``model_list`` arm pins ``litellm_params.aws_region_name`` / a geo-prefixed\n inference profile). This block adds a PER-REQUEST region preference + a hard\n residency constraint on top, applied as a PRE-SCORING candidate filter (the\n same ``filter_routable_candidates`` seam every RouteIQ strategy already calls,\n so it composes with gov-ban + cooldown and benefits EVERY strategy -- no new\n strategy subclass needed).\n\n The requested region is resolved per request, FIRST match wins:\n\n 1. The request **header** named by :attr:`region_header` (read\n case-insensitively from ``request_kwargs['headers']`` /\n ``metadata['headers']``) -- e.g. ``X-RouteIQ-Region: eu``.\n 2. An explicit ``region`` / ``residency_region`` key in\n ``request_kwargs`` / ``metadata`` (e.g. a governance-workspace region the\n auth layer stamped onto the request metadata).\n\n A request is \"constrained\" (HARD residency) when EITHER the resolved value\n came with a residency flag (``residency`` / ``data_residency`` truthy in the\n same source) OR :attr:`hard_residency_default` is True. A constrained request\n is NEVER routed out-of-region: if no in-region candidate exists the candidate\n set is emptied (the strategy then returns ``None`` / triggers a fallback) --\n a residency violation must never silently leak. An UNCONSTRAINED request\n PREFERS in-region but falls back to the full set when no in-region arm exists.\n\n A candidate's region is read from ``litellm_params.aws_region_name`` (the\n field every Bedrock arm already sets), then normalized through\n :attr:`region_map` so a request token (``\"eu\"``) can map to one or more\n concrete AWS regions (``[\"eu-west-1\", \"eu-central-1\"]``). When a request\n token is absent from ``region_map`` it is matched verbatim (so an\n ``aws_region_name``-equal request token works with zero map config).\n\n Env vars (``__`` nested delimiter): ``ROUTEIQ_REGION_ROUTING__ENABLED``,\n ``ROUTEIQ_REGION_ROUTING__REGION_HEADER``,\n ``ROUTEIQ_REGION_ROUTING__REGION_MAP`` (JSON map),\n ``ROUTEIQ_REGION_ROUTING__HARD_RESIDENCY_DEFAULT``.\n\n Default OFF / identity: when ``enabled`` is False the filter is a byte-stable\n no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request region-aware / data-residency candidate \"\n \"pre-filter. Default off -> identity (byte-stable no-op).\"\n ),\n )\n region_header: str = Field(\n \"X-RouteIQ-Region\",\n description=(\n \"Request header carrying the desired region/residency token (read \"\n \"case-insensitively). Checked before the request_kwargs/metadata \"\n \"region keys.\"\n ),\n )\n region_map: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request region TOKEN -> list of concrete AWS regions that \"\n 'satisfy it (e.g. {\"eu\": [\"eu-west-1\", \"eu-central-1\"]}). A token '\n \"absent from the map is matched verbatim against the candidate \"\n \"``aws_region_name``, so an exact-region token needs no map entry.\"\n ),\n )\n hard_residency_default: bool = Field(\n False,\n description=(\n \"When True, EVERY region-constrained request is treated as a HARD \"\n \"residency constraint (never routed out-of-region) even without a \"\n \"per-request residency flag. When False (default) only requests that \"\n \"carry an explicit residency flag are hard-constrained; an \"\n \"unflagged region is a soft preference (in-region first, full-set \"\n \"fallback).\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n access_group_enforcement: bool = Field(\n False,\n description=(\n \"RouteIQ-2fa1: enforce per-key/per-tool MCP access groups on the LIVE \"\n \"invocation path. When True, invoking a tool whose backing server \"\n \"declares ``metadata['access_groups']`` requires the caller to present \"\n \"at least one matching access group; a non-member call is REJECTED at \"\n \"``invoke_tool`` (the terminal path, before any outbound MCP call). \"\n \"When False (default) the access-group metadata is purely advisory \"\n \"(used only by the discovery/registry listing filter) -- byte-stable. \"\n \"Servers with NO ``access_groups`` configured are unrestricted on \"\n \"either setting (open by default). Env: \"\n \"``ROUTEIQ_MCP__ACCESS_GROUP_ENFORCEMENT``.\"\n ),\n )\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n agentcore_memory_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-60e7: persist A2A agent/conversation state into AWS Bedrock \"\n \"AgentCore Memory after a successful agent invocation (via the live \"\n \"bedrock-agentcore-mcp plugin's CreateEvent). Default OFF -- \"\n \"byte-stable: the A2A invocation path is unchanged and issues no \"\n \"AgentCore call until an operator opts in. Env: \"\n \"``ROUTEIQ_A2A__AGENTCORE_MEMORY_ENABLED`` (or A2A_AGENTCORE_MEMORY_ENABLED).\"\n ),\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass ApiSurfacesSettings(BaseModel):\n \"\"\"First-class API surfaces, persisted spend, and self-service keys\n (RouteIQ cluster N: e48a / 67fe / 3215).\n\n All flags DEFAULT to the byte-stable behaviour:\n\n * ``bare_paths_enabled`` -- register the bare-path delegators (Responses,\n rerank, RAG, batches, vector-store, audio, images) on the parent app so\n they get RouteIQ middleware + documentation (default ON; a no-op when\n LiteLLM is not mounted).\n * ``persisted_spend_enabled`` -- aggregate the spend report from the durable\n ``governance_spend`` rollup (default ON; degrades to the zero/no-op report\n automatically when no ``DATABASE_URL`` is configured).\n * ``self_service_keys_enabled`` -- expose the user-tier self-service key\n endpoints (default ON; they are scope-isolated to the caller's identity).\n\n Env vars: ``ROUTEIQ_API_SURFACES__BARE_PATHS_ENABLED``,\n ``ROUTEIQ_API_SURFACES__PERSISTED_SPEND_ENABLED``,\n ``ROUTEIQ_API_SURFACES__SELF_SERVICE_KEYS_ENABLED``.\n \"\"\"\n\n bare_paths_enabled: bool = Field(\n True,\n description=(\n \"Register first-class bare-path API surface delegators so they get \"\n \"RouteIQ middleware + OpenAPI documentation (no-op without the /v1 \"\n \"LiteLLM mount).\"\n ),\n )\n persisted_spend_enabled: bool = Field(\n True,\n description=(\n \"Aggregate the spend report from the durable governance_spend \"\n \"rollup (degrades to a zero report when no DATABASE_URL is set).\"\n ),\n )\n self_service_keys_enabled: bool = Field(\n True,\n description=(\n \"Expose user-tier self-service key endpoints (scope-isolated to the \"\n \"caller's identity).\"\n ),\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsFailureCaptureSettings(BaseModel):\n \"\"\"Low-rate capture from the eval COLLECT FAILURE/timeout path (RouteIQ-d365).\n\n The eval COLLECT arm only samples SUCCESS responses, so AGGREGATE/FEEDBACK\n never sees which models/strategies are error-prone. When enabled, a\n parallel, clearly-labeled, low-rate capture fires on the FAILURE path\n (``async_log_failure_event``) so the loop can downweight error-prone\n models/strategies.\n\n Captured failure samples are tagged (``outcome=\"failure\"`` +\n ``error_type``) so AGGREGATE never confuses them with successful responses.\n Default OFF and sampled at a LOW rate so a failure storm cannot flood the\n eval queue.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable low-rate capture of FAILURE/timeout-path eval samples.\",\n )\n sample_rate: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Fraction of failure events to capture (kept LOW so a failure storm \"\n \"does not flood the eval queue).\"\n ),\n )\n\n\nclass MLOpsUpstreamRouterSettings(BaseModel):\n \"\"\"Delegate routing to an upstream LiteLLM adaptive/quality/auto router\n (RouteIQ-8539).\n\n Upstream LiteLLM ships its OWN online-bandit / quality-feedback routing\n modes (``litellm.router_strategy.adaptive_router`` /\n ``quality_router`` / ``auto_router``), each configured per-deployment via the\n ``auto_router/...`` model prefix and holding an in-memory\n ``AdaptiveRouterUpdateQueue``. This block lets RouteIQ DELEGATE to one of\n those modes (alongside the RouteIQ ML strategies) and FLUSH the chosen\n router's update queue to the durable store on the MLOps cadence.\n\n Default OFF: with ``enabled=False`` RouteIQ never touches the upstream\n router registries, so this is a byte-stable no-op until an operator opts in.\n The upstream router itself must additionally be configured in the LiteLLM\n ``model_list`` (the ``auto_router/...`` deployment) -- this flag only wires\n RouteIQ's delegation + queue-flush, it does not create the deployment.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Delegate to an upstream LiteLLM adaptive/quality/auto router and \"\n \"flush its update queue to the durable store.\"\n ),\n )\n mode: str = Field(\n \"adaptive\",\n description=(\n \"Which upstream router family to delegate to: 'adaptive' \"\n \"(adaptive_router), 'quality' (quality_router), or 'auto' \"\n \"(auto_router). Selects which of the LiteLLM Router's per-name \"\n \"router dicts RouteIQ delegates to / flushes.\"\n ),\n )\n router_name: str = Field(\n \"\",\n description=(\n \"Optional specific upstream router_name (the model_list deployment \"\n \"name) to delegate to. Empty => the first registered router of the \"\n \"selected mode.\"\n ),\n )\n flush_queue: bool = Field(\n True,\n description=(\n \"On each MLOps feedback cadence, drain the selected upstream \"\n \"router's AdaptiveRouterUpdateQueue (state + session) to the durable \"\n \"store (the proxy prisma client). No-op when the mode has no queue \"\n \"(auto/quality) or no durable client is available.\"\n ),\n )\n\n\nclass MLOpsSageMakerSettings(BaseModel):\n \"\"\"SageMaker Model Registry + Experiments integration (RouteIQ-93e9).\n\n Registers a trained routing-model artifact into a SageMaker Model Package\n Group (with an approval status) and logs run metrics to SageMaker\n Experiments. Cred-free + mockable: the adapter takes an injected boto3\n sagemaker client, so unit tests pass a mock and never touch AWS.\n\n Default OFF: live SageMaker calls are operator-gated. With\n ``enabled=False`` the adapter's register/log methods short-circuit and never\n construct a boto3 client.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable SageMaker Model Registry + Experiments integration. Live \"\n \"SageMaker calls are operator-gated; default off.\"\n ),\n )\n model_package_group_name: str = Field(\n \"routeiq-routing-models\",\n description=\"SageMaker Model Package Group the routing artifact registers into.\",\n )\n approval_status: str = Field(\n \"PendingManualApproval\",\n description=(\n \"ModelApprovalStatus for the registered package \"\n \"(PendingManualApproval | Approved | Rejected).\"\n ),\n )\n experiment_name: str = Field(\n \"routeiq-routing-eval\",\n description=\"SageMaker Experiment name run metrics are logged under.\",\n )\n region: str = Field(\n \"\",\n description=\"AWS region for the boto3 sagemaker client (empty => default chain).\",\n )\n\n\nclass MLOpsOfflineEvalSettings(BaseModel):\n \"\"\"Offline eval harness over a versioned golden dataset (RouteIQ-8d24).\n\n Replays a versioned golden dataset (request -> expected-quality) through the\n eval judge / a strategy and reports per-strategy quality OFFLINE -- no live\n traffic. Default OFF; the harness is also invokable directly (tests / a CLI\n / a batch job) without flipping this flag.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the offline eval harness (also directly invokable).\",\n )\n golden_dataset_path: str = Field(\n \"\",\n description=(\n \"Path to the versioned golden dataset JSON. Empty => the bundled \"\n \"default fixture under config/golden_eval/.\"\n ),\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H + Cluster M).\n\n Bundles the MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), shadow/mirror canary traffic (RouteIQ-4fd1), upstream\n LiteLLM router delegation (RouteIQ-8539), SageMaker registry/experiments\n (RouteIQ-93e9), and the offline eval harness (RouteIQ-8d24). All are\n independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``,\n ``ROUTEIQ_MLOPS__UPSTREAM_ROUTER__ENABLED``,\n ``ROUTEIQ_MLOPS__SAGEMAKER__ENABLED``,\n ``ROUTEIQ_MLOPS__OFFLINE_EVAL__ENABLED``,\n ``ROUTEIQ_MLOPS__FAILURE_CAPTURE__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n upstream_router: MLOpsUpstreamRouterSettings = Field(\n default_factory=MLOpsUpstreamRouterSettings, # type: ignore[arg-type]\n description=\"Upstream LiteLLM adaptive/quality/auto router delegation.\",\n )\n sagemaker: MLOpsSageMakerSettings = Field(\n default_factory=MLOpsSageMakerSettings, # type: ignore[arg-type]\n description=\"SageMaker Model Registry + Experiments integration.\",\n )\n offline_eval: MLOpsOfflineEvalSettings = Field(\n default_factory=MLOpsOfflineEvalSettings, # type: ignore[arg-type]\n description=\"Offline eval harness over a versioned golden dataset.\",\n )\n failure_capture: MLOpsFailureCaptureSettings = Field(\n default_factory=MLOpsFailureCaptureSettings, # type: ignore[arg-type]\n description=\"Low-rate failure/timeout-path eval sample capture.\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n include_marketplace_endpoints: bool = Field(\n False,\n description=(\n \"RouteIQ-7105: also enumerate Amazon Bedrock Marketplace / mantle \"\n \"custom-deployment endpoints (``ListMarketplaceModelEndpoints``) and \"\n \"register each REGISTERED endpoint ARN as a ``model_list`` arm \"\n \"(``bedrock/<endpointArn>``). This extends the serverless-FM scan to \"\n \"cover operator-deployed Marketplace models (the bedrock-mantle \"\n \"PrivateLink custom endpoints). Default OFF -- byte-stable: the extra \"\n \"control call is opt-in and adds no arms until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__INCLUDE_MARKETPLACE_ENDPOINTS``.\"\n ),\n )\n drift_metric_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-9a42: emit a model-catalogue DRIFT metric \"\n \"(``gateway.bedrock.catalogue_drift``) measuring \"\n \"models_available (discovered) vs models_configured (the live \"\n \"model_list) so a CloudWatch alarm can fire when the deployed \"\n \"catalogue drifts from what Bedrock actually offers. Default OFF -- \"\n \"byte-stable: no metric is recorded until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_METRIC_ENABLED``.\"\n ),\n )\n drift_check_interval_seconds: float = Field(\n 900.0,\n ge=30.0,\n le=86400.0,\n description=(\n \"RouteIQ-9a42: interval (seconds) between periodic leader-gated \"\n \"catalogue-drift checks. Only consulted when \"\n \"``drift_metric_enabled`` AND ``enabled`` are True; otherwise the \"\n \"periodic task never starts (byte-stable). Default 900s (15 min). \"\n \"Env: ``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_CHECK_INTERVAL_SECONDS``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass BedrockRequestLeversSettings(BaseModel):\n \"\"\"Per-request Bedrock Converse levers (data plane), default-OFF.\n\n Drives :class:`litellm_llmrouter.gateway.plugins.bedrock_request_levers\n .BedrockRequestLeversPlugin`, which mutates the outbound request on the\n correct litellm seam (``CustomLogger.async_pre_call_deployment_hook``) so\n the mutations actually reach the Bedrock ``Converse`` call args.\n\n Three independently-gated levers (each a byte-stable no-op when off):\n\n - ``request_metadata`` (RouteIQ-294a): forward tenant identity\n (workspace / key) as Bedrock ``requestMetadata`` tags for cost\n attribution. Lands as the top-level ``requestMetadata`` completion kwarg,\n which litellm's converse transform maps from ``non_default_params``.\n - ``team_callbacks`` (RouteIQ-9cd8): set per-team logging sinks as the\n TOP-LEVEL request kwargs ``callbacks`` / ``success_callback`` /\n ``failure_callback`` (where litellm sources dynamic per-request\n callbacks), NOT ``metadata[callbacks]`` (never read). The global\n registration is unaffected.\n - ``cache_point`` (RouteIQ-b9ee): drive Bedrock prompt-caching via\n litellm's ``cache_control_injection_points`` mechanism (system-prefix\n message injection + tool_config cachePoint), NOT a bare ``tools[]``\n ``cachePoint`` entry (dropped by the converse transform).\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_BEDROCK_LEVERS__REQUEST_METADATA``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACKS``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_POINT``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_SYSTEM``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_TOOLS``,\n ``ROUTEIQ_BEDROCK_LEVERS__METADATA_PREFIX``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACK_MAP`` (JSON\n ``{\"team-a\": {\"success_callback\": [\"s3\"]}}``).\n \"\"\"\n\n request_metadata: bool = Field(\n False,\n description=(\n \"RouteIQ-294a: forward tenant identity (workspace_id / key_id) as \"\n \"Bedrock requestMetadata tags for cost attribution. Default OFF -- \"\n \"byte-stable: no requestMetadata kwarg is added until enabled.\"\n ),\n )\n metadata_prefix: str = Field(\n \"routeiq_\",\n description=(\n \"Key prefix for the injected requestMetadata tags (Bedrock keys must \"\n \"match [a-zA-Z0-9 :_@$#=/+,.-]). Only used when request_metadata=True.\"\n ),\n )\n team_callbacks: bool = Field(\n False,\n description=(\n \"RouteIQ-9cd8: set per-team logging sinks as TOP-LEVEL request kwargs \"\n \"callbacks/success_callback/failure_callback (the seam litellm reads \"\n \"dynamic per-request callbacks from). Default OFF; the global \"\n \"registration is untouched.\"\n ),\n )\n team_callback_map: dict[str, dict[str, list[str]]] = Field(\n default_factory=dict,\n description=(\n \"Map of team_id -> {success_callback|failure_callback|callbacks: \"\n '[sink names]}. e.g. {\"team-a\": {\"success_callback\": [\"s3\"]}}. Only '\n \"applied when team_callbacks=True and the request resolves to a \"\n \"mapped team.\"\n ),\n )\n cache_point: bool = Field(\n False,\n description=(\n \"RouteIQ-b9ee: enable Bedrock prompt-caching cachePoint injection via \"\n \"litellm cache_control_injection_points. Default OFF -- byte-stable.\"\n ),\n )\n cache_system: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a system-prefix cachePoint (message \"\n \"injection point, role=system). Default on.\"\n ),\n )\n cache_tools: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a tool_config cachePoint so the tool \"\n \"schema prefix is cached. Default on. Only takes effect when the \"\n \"request carries tools.\"\n ),\n )\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\nclass EngineMetricsSettings(BaseModel):\n \"\"\"Self-hosted-engine ``/metrics`` scrape configuration (default OFF).\n\n Drives :mod:`litellm_llmrouter.engine_metrics`, the credless scraper that\n GETs a self-hosted inference engine's Prometheus ``/metrics`` (vLLM, vLLM\n Production Stack, AIBrix, llm-d -- all serve the ``vllm:*`` family) and\n parses the queue-depth / KV-cache-pressure gauges\n (``vllm:num_requests_waiting``, ``vllm:kv_cache_usage_perc`` /\n ``vllm:gpu_cache_usage_perc``, ...). Those signals feed a *future*\n KV/queue-aware Layer-1 router or an autoscaler-into-the-engine.\n\n LAYERING: this reads the engine *frontend's* aggregate ``/metrics`` so\n Layer-1 can decide whether to send more load to this engine arm; it never\n scrapes individual worker pods (that would collapse the two-layer model --\n see ``docs/architecture/aws-rearchitecture/51-multinode-large-model-serving.md``\n Part 3).\n\n Default DISABLED (opt-in): with ``enabled=False`` the scraper is a byte-stable\n no-op -- importing the module does no I/O, and ``scrape()`` returns an empty\n ``reachable=False`` snapshot without touching the network. Live scraping is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_ENGINE_METRICS__ENABLED``,\n ``ROUTEIQ_ENGINE_METRICS__SCRAPE_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the self-hosted-engine /metrics scrape. Default OFF -- the \"\n \"scraper no-ops and does zero network I/O until an operator opts in \"\n \"(live scrape is operator-gated).\"\n ),\n )\n scrape_timeout: float = Field(\n 2.0,\n ge=0.1,\n le=30.0,\n description=(\n \"Per-scrape HTTP GET timeout in seconds. An engine that does not \"\n \"answer within this budget yields an empty (reachable=False) \"\n \"snapshot, never an exception.\"\n ),\n )\n kv_aware_routing_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-08d6/6a89: enable the KV-cache-/queue-aware routing path for \"\n \"self-hosted engine arms. When ON the router scrapes each candidate \"\n \"arm's engine /metrics (vllm:num_requests_waiting, kv_cache_usage_perc) \"\n \"and selects the LEAST-LOADED arm. Requires ``enabled`` (the scraper) \"\n \"to also be True. Default OFF -- byte-stable: the KV-aware pre-step is \"\n \"skipped entirely and routing is unchanged. Env: \"\n \"``ROUTEIQ_ENGINE_METRICS__KV_AWARE_ROUTING_ENABLED``.\"\n ),\n )\n kv_cache_usage_weight: float = Field(\n 1.0,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's KV-cache usage fraction (0..1) when \"\n \"scoring engine load for KV-aware routing. Higher => avoid \"\n \"cache-pressured arms more aggressively.\"\n ),\n )\n waiting_queue_weight: float = Field(\n 0.1,\n ge=0.0,\n le=100.0,\n description=(\n \"Weight applied to an arm's num_requests_waiting (queue depth) when \"\n \"scoring engine load for KV-aware routing. The load score is \"\n \"kv_cache_usage_weight*kv_usage + waiting_queue_weight*num_waiting.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n api_surfaces: ApiSurfacesSettings = Field(\n default_factory=ApiSurfacesSettings, # type: ignore[arg-type]\n description=(\n \"First-class API surfaces, persisted spend, and self-service key \"\n \"settings (cluster N: RouteIQ-e48a / 67fe / 3215; all default on / \"\n \"byte-stable).\"\n ),\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n bedrock_levers: BedrockRequestLeversSettings = Field(\n default_factory=BedrockRequestLeversSettings, # type: ignore[arg-type]\n description=(\n \"Per-request Bedrock Converse levers (requestMetadata / team \"\n \"callbacks / cachePoint) on the pre-call mutation seam (default off).\"\n ),\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n region_routing: RegionRoutingSettings = Field(\n default_factory=RegionRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Per-request region-aware / data-residency candidate-filter settings \"\n \"(default off / identity).\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n engine_metrics: EngineMetricsSettings = Field(\n default_factory=EngineMetricsSettings, # type: ignore[arg-type]\n description=(\n \"Self-hosted-engine /metrics scrape settings (vLLM / Production \"\n \"Stack / AIBrix / llm-d queue-depth + KV-cache gauges; default off).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "87e543e96bf80bc95381b8e02dca7844cdf8d6ac": {"t": 1785769074.1674016, "n": 119452, "h": "abd4bc7e9e747efc", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass RegionRoutingSettings(BaseModel):\n \"\"\"Per-request region-aware / data-residency candidate-filter configuration\n (RouteIQ-60cc).\n\n Bedrock cross-region inference profiles are already used STATICALLY (each\n ``model_list`` arm pins ``litellm_params.aws_region_name`` / a geo-prefixed\n inference profile). This block adds a PER-REQUEST region preference + a hard\n residency constraint on top, applied as a PRE-SCORING candidate filter (the\n same ``filter_routable_candidates`` seam every RouteIQ strategy already calls,\n so it composes with gov-ban + cooldown and benefits EVERY strategy -- no new\n strategy subclass needed).\n\n The requested region is resolved per request, FIRST match wins:\n\n 1. The request **header** named by :attr:`region_header` (read\n case-insensitively from ``request_kwargs['headers']`` /\n ``metadata['headers']``) -- e.g. ``X-RouteIQ-Region: eu``.\n 2. An explicit ``region`` / ``residency_region`` key in\n ``request_kwargs`` / ``metadata`` (e.g. a governance-workspace region the\n auth layer stamped onto the request metadata).\n\n A request is \"constrained\" (HARD residency) when EITHER the resolved value\n came with a residency flag (``residency`` / ``data_residency`` truthy in the\n same source) OR :attr:`hard_residency_default` is True. A constrained request\n is NEVER routed out-of-region: if no in-region candidate exists the candidate\n set is emptied (the strategy then returns ``None`` / triggers a fallback) --\n a residency violation must never silently leak. An UNCONSTRAINED request\n PREFERS in-region but falls back to the full set when no in-region arm exists.\n\n A candidate's region is read from ``litellm_params.aws_region_name`` (the\n field every Bedrock arm already sets), then normalized through\n :attr:`region_map` so a request token (``\"eu\"``) can map to one or more\n concrete AWS regions (``[\"eu-west-1\", \"eu-central-1\"]``). When a request\n token is absent from ``region_map`` it is matched verbatim (so an\n ``aws_region_name``-equal request token works with zero map config).\n\n Env vars (``__`` nested delimiter): ``ROUTEIQ_REGION_ROUTING__ENABLED``,\n ``ROUTEIQ_REGION_ROUTING__REGION_HEADER``,\n ``ROUTEIQ_REGION_ROUTING__REGION_MAP`` (JSON map),\n ``ROUTEIQ_REGION_ROUTING__HARD_RESIDENCY_DEFAULT``.\n\n Default OFF / identity: when ``enabled`` is False the filter is a byte-stable\n no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request region-aware / data-residency candidate \"\n \"pre-filter. Default off -> identity (byte-stable no-op).\"\n ),\n )\n region_header: str = Field(\n \"X-RouteIQ-Region\",\n description=(\n \"Request header carrying the desired region/residency token (read \"\n \"case-insensitively). Checked before the request_kwargs/metadata \"\n \"region keys.\"\n ),\n )\n region_map: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request region TOKEN -> list of concrete AWS regions that \"\n 'satisfy it (e.g. {\"eu\": [\"eu-west-1\", \"eu-central-1\"]}). A token '\n \"absent from the map is matched verbatim against the candidate \"\n \"``aws_region_name``, so an exact-region token needs no map entry.\"\n ),\n )\n hard_residency_default: bool = Field(\n False,\n description=(\n \"When True, EVERY region-constrained request is treated as a HARD \"\n \"residency constraint (never routed out-of-region) even without a \"\n \"per-request residency flag. When False (default) only requests that \"\n \"carry an explicit residency flag are hard-constrained; an \"\n \"unflagged region is a soft preference (in-region first, full-set \"\n \"fallback).\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n access_group_enforcement: bool = Field(\n False,\n description=(\n \"RouteIQ-2fa1: enforce per-key/per-tool MCP access groups on the LIVE \"\n \"invocation path. When True, invoking a tool whose backing server \"\n \"declares ``metadata['access_groups']`` requires the caller to present \"\n \"at least one matching access group; a non-member call is REJECTED at \"\n \"``invoke_tool`` (the terminal path, before any outbound MCP call). \"\n \"When False (default) the access-group metadata is purely advisory \"\n \"(used only by the discovery/registry listing filter) -- byte-stable. \"\n \"Servers with NO ``access_groups`` configured are unrestricted on \"\n \"either setting (open by default). Env: \"\n \"``ROUTEIQ_MCP__ACCESS_GROUP_ENFORCEMENT``.\"\n ),\n )\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass ApiSurfacesSettings(BaseModel):\n \"\"\"First-class API surfaces, persisted spend, and self-service keys\n (RouteIQ cluster N: e48a / 67fe / 3215).\n\n All flags DEFAULT to the byte-stable behaviour:\n\n * ``bare_paths_enabled`` -- register the bare-path delegators (Responses,\n rerank, RAG, batches, vector-store, audio, images) on the parent app so\n they get RouteIQ middleware + documentation (default ON; a no-op when\n LiteLLM is not mounted).\n * ``persisted_spend_enabled`` -- aggregate the spend report from the durable\n ``governance_spend`` rollup (default ON; degrades to the zero/no-op report\n automatically when no ``DATABASE_URL`` is configured).\n * ``self_service_keys_enabled`` -- expose the user-tier self-service key\n endpoints (default ON; they are scope-isolated to the caller's identity).\n\n Env vars: ``ROUTEIQ_API_SURFACES__BARE_PATHS_ENABLED``,\n ``ROUTEIQ_API_SURFACES__PERSISTED_SPEND_ENABLED``,\n ``ROUTEIQ_API_SURFACES__SELF_SERVICE_KEYS_ENABLED``.\n \"\"\"\n\n bare_paths_enabled: bool = Field(\n True,\n description=(\n \"Register first-class bare-path API surface delegators so they get \"\n \"RouteIQ middleware + OpenAPI documentation (no-op without the /v1 \"\n \"LiteLLM mount).\"\n ),\n )\n persisted_spend_enabled: bool = Field(\n True,\n description=(\n \"Aggregate the spend report from the durable governance_spend \"\n \"rollup (degrades to a zero report when no DATABASE_URL is set).\"\n ),\n )\n self_service_keys_enabled: bool = Field(\n True,\n description=(\n \"Expose user-tier self-service key endpoints (scope-isolated to the \"\n \"caller's identity).\"\n ),\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsFailureCaptureSettings(BaseModel):\n \"\"\"Low-rate capture from the eval COLLECT FAILURE/timeout path (RouteIQ-d365).\n\n The eval COLLECT arm only samples SUCCESS responses, so AGGREGATE/FEEDBACK\n never sees which models/strategies are error-prone. When enabled, a\n parallel, clearly-labeled, low-rate capture fires on the FAILURE path\n (``async_log_failure_event``) so the loop can downweight error-prone\n models/strategies.\n\n Captured failure samples are tagged (``outcome=\"failure\"`` +\n ``error_type``) so AGGREGATE never confuses them with successful responses.\n Default OFF and sampled at a LOW rate so a failure storm cannot flood the\n eval queue.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable low-rate capture of FAILURE/timeout-path eval samples.\",\n )\n sample_rate: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Fraction of failure events to capture (kept LOW so a failure storm \"\n \"does not flood the eval queue).\"\n ),\n )\n\n\nclass MLOpsUpstreamRouterSettings(BaseModel):\n \"\"\"Delegate routing to an upstream LiteLLM adaptive/quality/auto router\n (RouteIQ-8539).\n\n Upstream LiteLLM ships its OWN online-bandit / quality-feedback routing\n modes (``litellm.router_strategy.adaptive_router`` /\n ``quality_router`` / ``auto_router``), each configured per-deployment via the\n ``auto_router/...`` model prefix and holding an in-memory\n ``AdaptiveRouterUpdateQueue``. This block lets RouteIQ DELEGATE to one of\n those modes (alongside the RouteIQ ML strategies) and FLUSH the chosen\n router's update queue to the durable store on the MLOps cadence.\n\n Default OFF: with ``enabled=False`` RouteIQ never touches the upstream\n router registries, so this is a byte-stable no-op until an operator opts in.\n The upstream router itself must additionally be configured in the LiteLLM\n ``model_list`` (the ``auto_router/...`` deployment) -- this flag only wires\n RouteIQ's delegation + queue-flush, it does not create the deployment.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Delegate to an upstream LiteLLM adaptive/quality/auto router and \"\n \"flush its update queue to the durable store.\"\n ),\n )\n mode: str = Field(\n \"adaptive\",\n description=(\n \"Which upstream router family to delegate to: 'adaptive' \"\n \"(adaptive_router), 'quality' (quality_router), or 'auto' \"\n \"(auto_router). Selects which of the LiteLLM Router's per-name \"\n \"router dicts RouteIQ delegates to / flushes.\"\n ),\n )\n router_name: str = Field(\n \"\",\n description=(\n \"Optional specific upstream router_name (the model_list deployment \"\n \"name) to delegate to. Empty => the first registered router of the \"\n \"selected mode.\"\n ),\n )\n flush_queue: bool = Field(\n True,\n description=(\n \"On each MLOps feedback cadence, drain the selected upstream \"\n \"router's AdaptiveRouterUpdateQueue (state + session) to the durable \"\n \"store (the proxy prisma client). No-op when the mode has no queue \"\n \"(auto/quality) or no durable client is available.\"\n ),\n )\n\n\nclass MLOpsSageMakerSettings(BaseModel):\n \"\"\"SageMaker Model Registry + Experiments integration (RouteIQ-93e9).\n\n Registers a trained routing-model artifact into a SageMaker Model Package\n Group (with an approval status) and logs run metrics to SageMaker\n Experiments. Cred-free + mockable: the adapter takes an injected boto3\n sagemaker client, so unit tests pass a mock and never touch AWS.\n\n Default OFF: live SageMaker calls are operator-gated. With\n ``enabled=False`` the adapter's register/log methods short-circuit and never\n construct a boto3 client.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable SageMaker Model Registry + Experiments integration. Live \"\n \"SageMaker calls are operator-gated; default off.\"\n ),\n )\n model_package_group_name: str = Field(\n \"routeiq-routing-models\",\n description=\"SageMaker Model Package Group the routing artifact registers into.\",\n )\n approval_status: str = Field(\n \"PendingManualApproval\",\n description=(\n \"ModelApprovalStatus for the registered package \"\n \"(PendingManualApproval | Approved | Rejected).\"\n ),\n )\n experiment_name: str = Field(\n \"routeiq-routing-eval\",\n description=\"SageMaker Experiment name run metrics are logged under.\",\n )\n region: str = Field(\n \"\",\n description=\"AWS region for the boto3 sagemaker client (empty => default chain).\",\n )\n\n\nclass MLOpsOfflineEvalSettings(BaseModel):\n \"\"\"Offline eval harness over a versioned golden dataset (RouteIQ-8d24).\n\n Replays a versioned golden dataset (request -> expected-quality) through the\n eval judge / a strategy and reports per-strategy quality OFFLINE -- no live\n traffic. Default OFF; the harness is also invokable directly (tests / a CLI\n / a batch job) without flipping this flag.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the offline eval harness (also directly invokable).\",\n )\n golden_dataset_path: str = Field(\n \"\",\n description=(\n \"Path to the versioned golden dataset JSON. Empty => the bundled \"\n \"default fixture under config/golden_eval/.\"\n ),\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H + Cluster M).\n\n Bundles the MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), shadow/mirror canary traffic (RouteIQ-4fd1), upstream\n LiteLLM router delegation (RouteIQ-8539), SageMaker registry/experiments\n (RouteIQ-93e9), and the offline eval harness (RouteIQ-8d24). All are\n independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``,\n ``ROUTEIQ_MLOPS__UPSTREAM_ROUTER__ENABLED``,\n ``ROUTEIQ_MLOPS__SAGEMAKER__ENABLED``,\n ``ROUTEIQ_MLOPS__OFFLINE_EVAL__ENABLED``,\n ``ROUTEIQ_MLOPS__FAILURE_CAPTURE__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n upstream_router: MLOpsUpstreamRouterSettings = Field(\n default_factory=MLOpsUpstreamRouterSettings, # type: ignore[arg-type]\n description=\"Upstream LiteLLM adaptive/quality/auto router delegation.\",\n )\n sagemaker: MLOpsSageMakerSettings = Field(\n default_factory=MLOpsSageMakerSettings, # type: ignore[arg-type]\n description=\"SageMaker Model Registry + Experiments integration.\",\n )\n offline_eval: MLOpsOfflineEvalSettings = Field(\n default_factory=MLOpsOfflineEvalSettings, # type: ignore[arg-type]\n description=\"Offline eval harness over a versioned golden dataset.\",\n )\n failure_capture: MLOpsFailureCaptureSettings = Field(\n default_factory=MLOpsFailureCaptureSettings, # type: ignore[arg-type]\n description=\"Low-rate failure/timeout-path eval sample capture.\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n include_marketplace_endpoints: bool = Field(\n False,\n description=(\n \"RouteIQ-7105: also enumerate Amazon Bedrock Marketplace / mantle \"\n \"custom-deployment endpoints (``ListMarketplaceModelEndpoints``) and \"\n \"register each REGISTERED endpoint ARN as a ``model_list`` arm \"\n \"(``bedrock/<endpointArn>``). This extends the serverless-FM scan to \"\n \"cover operator-deployed Marketplace models (the bedrock-mantle \"\n \"PrivateLink custom endpoints). Default OFF -- byte-stable: the extra \"\n \"control call is opt-in and adds no arms until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__INCLUDE_MARKETPLACE_ENDPOINTS``.\"\n ),\n )\n drift_metric_enabled: bool = Field(\n False,\n description=(\n \"RouteIQ-9a42: emit a model-catalogue DRIFT metric \"\n \"(``gateway.bedrock.catalogue_drift``) measuring \"\n \"models_available (discovered) vs models_configured (the live \"\n \"model_list) so a CloudWatch alarm can fire when the deployed \"\n \"catalogue drifts from what Bedrock actually offers. Default OFF -- \"\n \"byte-stable: no metric is recorded until enabled. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__DRIFT_METRIC_ENABLED``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass BedrockRequestLeversSettings(BaseModel):\n \"\"\"Per-request Bedrock Converse levers (data plane), default-OFF.\n\n Drives :class:`litellm_llmrouter.gateway.plugins.bedrock_request_levers\n .BedrockRequestLeversPlugin`, which mutates the outbound request on the\n correct litellm seam (``CustomLogger.async_pre_call_deployment_hook``) so\n the mutations actually reach the Bedrock ``Converse`` call args.\n\n Three independently-gated levers (each a byte-stable no-op when off):\n\n - ``request_metadata`` (RouteIQ-294a): forward tenant identity\n (workspace / key) as Bedrock ``requestMetadata`` tags for cost\n attribution. Lands as the top-level ``requestMetadata`` completion kwarg,\n which litellm's converse transform maps from ``non_default_params``.\n - ``team_callbacks`` (RouteIQ-9cd8): set per-team logging sinks as the\n TOP-LEVEL request kwargs ``callbacks`` / ``success_callback`` /\n ``failure_callback`` (where litellm sources dynamic per-request\n callbacks), NOT ``metadata[callbacks]`` (never read). The global\n registration is unaffected.\n - ``cache_point`` (RouteIQ-b9ee): drive Bedrock prompt-caching via\n litellm's ``cache_control_injection_points`` mechanism (system-prefix\n message injection + tool_config cachePoint), NOT a bare ``tools[]``\n ``cachePoint`` entry (dropped by the converse transform).\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_BEDROCK_LEVERS__REQUEST_METADATA``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACKS``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_POINT``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_SYSTEM``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_TOOLS``,\n ``ROUTEIQ_BEDROCK_LEVERS__METADATA_PREFIX``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACK_MAP`` (JSON\n ``{\"team-a\": {\"success_callback\": [\"s3\"]}}``).\n \"\"\"\n\n request_metadata: bool = Field(\n False,\n description=(\n \"RouteIQ-294a: forward tenant identity (workspace_id / key_id) as \"\n \"Bedrock requestMetadata tags for cost attribution. Default OFF -- \"\n \"byte-stable: no requestMetadata kwarg is added until enabled.\"\n ),\n )\n metadata_prefix: str = Field(\n \"routeiq_\",\n description=(\n \"Key prefix for the injected requestMetadata tags (Bedrock keys must \"\n \"match [a-zA-Z0-9 :_@$#=/+,.-]). Only used when request_metadata=True.\"\n ),\n )\n team_callbacks: bool = Field(\n False,\n description=(\n \"RouteIQ-9cd8: set per-team logging sinks as TOP-LEVEL request kwargs \"\n \"callbacks/success_callback/failure_callback (the seam litellm reads \"\n \"dynamic per-request callbacks from). Default OFF; the global \"\n \"registration is untouched.\"\n ),\n )\n team_callback_map: dict[str, dict[str, list[str]]] = Field(\n default_factory=dict,\n description=(\n \"Map of team_id -> {success_callback|failure_callback|callbacks: \"\n '[sink names]}. e.g. {\"team-a\": {\"success_callback\": [\"s3\"]}}. Only '\n \"applied when team_callbacks=True and the request resolves to a \"\n \"mapped team.\"\n ),\n )\n cache_point: bool = Field(\n False,\n description=(\n \"RouteIQ-b9ee: enable Bedrock prompt-caching cachePoint injection via \"\n \"litellm cache_control_injection_points. Default OFF -- byte-stable.\"\n ),\n )\n cache_system: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a system-prefix cachePoint (message \"\n \"injection point, role=system). Default on.\"\n ),\n )\n cache_tools: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a tool_config cachePoint so the tool \"\n \"schema prefix is cached. Default on. Only takes effect when the \"\n \"request carries tools.\"\n ),\n )\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\nclass EngineMetricsSettings(BaseModel):\n \"\"\"Self-hosted-engine ``/metrics`` scrape configuration (default OFF).\n\n Drives :mod:`litellm_llmrouter.engine_metrics`, the credless scraper that\n GETs a self-hosted inference engine's Prometheus ``/metrics`` (vLLM, vLLM\n Production Stack, AIBrix, llm-d -- all serve the ``vllm:*`` family) and\n parses the queue-depth / KV-cache-pressure gauges\n (``vllm:num_requests_waiting``, ``vllm:kv_cache_usage_perc`` /\n ``vllm:gpu_cache_usage_perc``, ...). Those signals feed a *future*\n KV/queue-aware Layer-1 router or an autoscaler-into-the-engine.\n\n LAYERING: this reads the engine *frontend's* aggregate ``/metrics`` so\n Layer-1 can decide whether to send more load to this engine arm; it never\n scrapes individual worker pods (that would collapse the two-layer model --\n see ``docs/architecture/aws-rearchitecture/51-multinode-large-model-serving.md``\n Part 3).\n\n Default DISABLED (opt-in): with ``enabled=False`` the scraper is a byte-stable\n no-op -- importing the module does no I/O, and ``scrape()`` returns an empty\n ``reachable=False`` snapshot without touching the network. Live scraping is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_ENGINE_METRICS__ENABLED``,\n ``ROUTEIQ_ENGINE_METRICS__SCRAPE_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the self-hosted-engine /metrics scrape. Default OFF -- the \"\n \"scraper no-ops and does zero network I/O until an operator opts in \"\n \"(live scrape is operator-gated).\"\n ),\n )\n scrape_timeout: float = Field(\n 2.0,\n ge=0.1,\n le=30.0,\n description=(\n \"Per-scrape HTTP GET timeout in seconds. An engine that does not \"\n \"answer within this budget yields an empty (reachable=False) \"\n \"snapshot, never an exception.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n api_surfaces: ApiSurfacesSettings = Field(\n default_factory=ApiSurfacesSettings, # type: ignore[arg-type]\n description=(\n \"First-class API surfaces, persisted spend, and self-service key \"\n \"settings (cluster N: RouteIQ-e48a / 67fe / 3215; all default on / \"\n \"byte-stable).\"\n ),\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n bedrock_levers: BedrockRequestLeversSettings = Field(\n default_factory=BedrockRequestLeversSettings, # type: ignore[arg-type]\n description=(\n \"Per-request Bedrock Converse levers (requestMetadata / team \"\n \"callbacks / cachePoint) on the pre-call mutation seam (default off).\"\n ),\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n region_routing: RegionRoutingSettings = Field(\n default_factory=RegionRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Per-request region-aware / data-residency candidate-filter settings \"\n \"(default off / identity).\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n engine_metrics: EngineMetricsSettings = Field(\n default_factory=EngineMetricsSettings, # type: ignore[arg-type]\n description=(\n \"Self-hosted-engine /metrics scrape settings (vLLM / Production \"\n \"Stack / AIBrix / llm-d queue-depth + KV-cache gauges; default off).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "d0cbf20dd53fc07969deb1e804aad10d9ab17a83": {"t": 1785769076.0353835, "n": 110417, "h": "04ac832168cce843", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass RegionRoutingSettings(BaseModel):\n \"\"\"Per-request region-aware / data-residency candidate-filter configuration\n (RouteIQ-60cc).\n\n Bedrock cross-region inference profiles are already used STATICALLY (each\n ``model_list`` arm pins ``litellm_params.aws_region_name`` / a geo-prefixed\n inference profile). This block adds a PER-REQUEST region preference + a hard\n residency constraint on top, applied as a PRE-SCORING candidate filter (the\n same ``filter_routable_candidates`` seam every RouteIQ strategy already calls,\n so it composes with gov-ban + cooldown and benefits EVERY strategy -- no new\n strategy subclass needed).\n\n The requested region is resolved per request, FIRST match wins:\n\n 1. The request **header** named by :attr:`region_header` (read\n case-insensitively from ``request_kwargs['headers']`` /\n ``metadata['headers']``) -- e.g. ``X-RouteIQ-Region: eu``.\n 2. An explicit ``region`` / ``residency_region`` key in\n ``request_kwargs`` / ``metadata`` (e.g. a governance-workspace region the\n auth layer stamped onto the request metadata).\n\n A request is \"constrained\" (HARD residency) when EITHER the resolved value\n came with a residency flag (``residency`` / ``data_residency`` truthy in the\n same source) OR :attr:`hard_residency_default` is True. A constrained request\n is NEVER routed out-of-region: if no in-region candidate exists the candidate\n set is emptied (the strategy then returns ``None`` / triggers a fallback) --\n a residency violation must never silently leak. An UNCONSTRAINED request\n PREFERS in-region but falls back to the full set when no in-region arm exists.\n\n A candidate's region is read from ``litellm_params.aws_region_name`` (the\n field every Bedrock arm already sets), then normalized through\n :attr:`region_map` so a request token (``\"eu\"``) can map to one or more\n concrete AWS regions (``[\"eu-west-1\", \"eu-central-1\"]``). When a request\n token is absent from ``region_map`` it is matched verbatim (so an\n ``aws_region_name``-equal request token works with zero map config).\n\n Env vars (``__`` nested delimiter): ``ROUTEIQ_REGION_ROUTING__ENABLED``,\n ``ROUTEIQ_REGION_ROUTING__REGION_HEADER``,\n ``ROUTEIQ_REGION_ROUTING__REGION_MAP`` (JSON map),\n ``ROUTEIQ_REGION_ROUTING__HARD_RESIDENCY_DEFAULT``.\n\n Default OFF / identity: when ``enabled`` is False the filter is a byte-stable\n no-op (returns the input list object unchanged).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the per-request region-aware / data-residency candidate \"\n \"pre-filter. Default off -> identity (byte-stable no-op).\"\n ),\n )\n region_header: str = Field(\n \"X-RouteIQ-Region\",\n description=(\n \"Request header carrying the desired region/residency token (read \"\n \"case-insensitively). Checked before the request_kwargs/metadata \"\n \"region keys.\"\n ),\n )\n region_map: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request region TOKEN -> list of concrete AWS regions that \"\n 'satisfy it (e.g. {\"eu\": [\"eu-west-1\", \"eu-central-1\"]}). A token '\n \"absent from the map is matched verbatim against the candidate \"\n \"``aws_region_name``, so an exact-region token needs no map entry.\"\n ),\n )\n hard_residency_default: bool = Field(\n False,\n description=(\n \"When True, EVERY region-constrained request is treated as a HARD \"\n \"residency constraint (never routed out-of-region) even without a \"\n \"per-request residency flag. When False (default) only requests that \"\n \"carry an explicit residency flag are hard-constrained; an \"\n \"unflagged region is a soft preference (in-region first, full-set \"\n \"fallback).\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass ApiSurfacesSettings(BaseModel):\n \"\"\"First-class API surfaces, persisted spend, and self-service keys\n (RouteIQ cluster N: e48a / 67fe / 3215).\n\n All flags DEFAULT to the byte-stable behaviour:\n\n * ``bare_paths_enabled`` -- register the bare-path delegators (Responses,\n rerank, RAG, batches, vector-store, audio, images) on the parent app so\n they get RouteIQ middleware + documentation (default ON; a no-op when\n LiteLLM is not mounted).\n * ``persisted_spend_enabled`` -- aggregate the spend report from the durable\n ``governance_spend`` rollup (default ON; degrades to the zero/no-op report\n automatically when no ``DATABASE_URL`` is configured).\n * ``self_service_keys_enabled`` -- expose the user-tier self-service key\n endpoints (default ON; they are scope-isolated to the caller's identity).\n\n Env vars: ``ROUTEIQ_API_SURFACES__BARE_PATHS_ENABLED``,\n ``ROUTEIQ_API_SURFACES__PERSISTED_SPEND_ENABLED``,\n ``ROUTEIQ_API_SURFACES__SELF_SERVICE_KEYS_ENABLED``.\n \"\"\"\n\n bare_paths_enabled: bool = Field(\n True,\n description=(\n \"Register first-class bare-path API surface delegators so they get \"\n \"RouteIQ middleware + OpenAPI documentation (no-op without the /v1 \"\n \"LiteLLM mount).\"\n ),\n )\n persisted_spend_enabled: bool = Field(\n True,\n description=(\n \"Aggregate the spend report from the durable governance_spend \"\n \"rollup (degrades to a zero report when no DATABASE_URL is set).\"\n ),\n )\n self_service_keys_enabled: bool = Field(\n True,\n description=(\n \"Expose user-tier self-service key endpoints (scope-isolated to the \"\n \"caller's identity).\"\n ),\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H).\n\n Bundles the three MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), and shadow/mirror canary traffic (RouteIQ-4fd1). All three\n are independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass BedrockRequestLeversSettings(BaseModel):\n \"\"\"Per-request Bedrock Converse levers (data plane), default-OFF.\n\n Drives :class:`litellm_llmrouter.gateway.plugins.bedrock_request_levers\n .BedrockRequestLeversPlugin`, which mutates the outbound request on the\n correct litellm seam (``CustomLogger.async_pre_call_deployment_hook``) so\n the mutations actually reach the Bedrock ``Converse`` call args.\n\n Three independently-gated levers (each a byte-stable no-op when off):\n\n - ``request_metadata`` (RouteIQ-294a): forward tenant identity\n (workspace / key) as Bedrock ``requestMetadata`` tags for cost\n attribution. Lands as the top-level ``requestMetadata`` completion kwarg,\n which litellm's converse transform maps from ``non_default_params``.\n - ``team_callbacks`` (RouteIQ-9cd8): set per-team logging sinks as the\n TOP-LEVEL request kwargs ``callbacks`` / ``success_callback`` /\n ``failure_callback`` (where litellm sources dynamic per-request\n callbacks), NOT ``metadata[callbacks]`` (never read). The global\n registration is unaffected.\n - ``cache_point`` (RouteIQ-b9ee): drive Bedrock prompt-caching via\n litellm's ``cache_control_injection_points`` mechanism (system-prefix\n message injection + tool_config cachePoint), NOT a bare ``tools[]``\n ``cachePoint`` entry (dropped by the converse transform).\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_BEDROCK_LEVERS__REQUEST_METADATA``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACKS``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_POINT``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_SYSTEM``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_TOOLS``,\n ``ROUTEIQ_BEDROCK_LEVERS__METADATA_PREFIX``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACK_MAP`` (JSON\n ``{\"team-a\": {\"success_callback\": [\"s3\"]}}``).\n \"\"\"\n\n request_metadata: bool = Field(\n False,\n description=(\n \"RouteIQ-294a: forward tenant identity (workspace_id / key_id) as \"\n \"Bedrock requestMetadata tags for cost attribution. Default OFF -- \"\n \"byte-stable: no requestMetadata kwarg is added until enabled.\"\n ),\n )\n metadata_prefix: str = Field(\n \"routeiq_\",\n description=(\n \"Key prefix for the injected requestMetadata tags (Bedrock keys must \"\n \"match [a-zA-Z0-9 :_@$#=/+,.-]). Only used when request_metadata=True.\"\n ),\n )\n team_callbacks: bool = Field(\n False,\n description=(\n \"RouteIQ-9cd8: set per-team logging sinks as TOP-LEVEL request kwargs \"\n \"callbacks/success_callback/failure_callback (the seam litellm reads \"\n \"dynamic per-request callbacks from). Default OFF; the global \"\n \"registration is untouched.\"\n ),\n )\n team_callback_map: dict[str, dict[str, list[str]]] = Field(\n default_factory=dict,\n description=(\n \"Map of team_id -> {success_callback|failure_callback|callbacks: \"\n '[sink names]}. e.g. {\"team-a\": {\"success_callback\": [\"s3\"]}}. Only '\n \"applied when team_callbacks=True and the request resolves to a \"\n \"mapped team.\"\n ),\n )\n cache_point: bool = Field(\n False,\n description=(\n \"RouteIQ-b9ee: enable Bedrock prompt-caching cachePoint injection via \"\n \"litellm cache_control_injection_points. Default OFF -- byte-stable.\"\n ),\n )\n cache_system: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a system-prefix cachePoint (message \"\n \"injection point, role=system). Default on.\"\n ),\n )\n cache_tools: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a tool_config cachePoint so the tool \"\n \"schema prefix is cached. Default on. Only takes effect when the \"\n \"request carries tools.\"\n ),\n )\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\nclass EngineMetricsSettings(BaseModel):\n \"\"\"Self-hosted-engine ``/metrics`` scrape configuration (default OFF).\n\n Drives :mod:`litellm_llmrouter.engine_metrics`, the credless scraper that\n GETs a self-hosted inference engine's Prometheus ``/metrics`` (vLLM, vLLM\n Production Stack, AIBrix, llm-d -- all serve the ``vllm:*`` family) and\n parses the queue-depth / KV-cache-pressure gauges\n (``vllm:num_requests_waiting``, ``vllm:kv_cache_usage_perc`` /\n ``vllm:gpu_cache_usage_perc``, ...). Those signals feed a *future*\n KV/queue-aware Layer-1 router or an autoscaler-into-the-engine.\n\n LAYERING: this reads the engine *frontend's* aggregate ``/metrics`` so\n Layer-1 can decide whether to send more load to this engine arm; it never\n scrapes individual worker pods (that would collapse the two-layer model --\n see ``docs/architecture/aws-rearchitecture/51-multinode-large-model-serving.md``\n Part 3).\n\n Default DISABLED (opt-in): with ``enabled=False`` the scraper is a byte-stable\n no-op -- importing the module does no I/O, and ``scrape()`` returns an empty\n ``reachable=False`` snapshot without touching the network. Live scraping is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_ENGINE_METRICS__ENABLED``,\n ``ROUTEIQ_ENGINE_METRICS__SCRAPE_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the self-hosted-engine /metrics scrape. Default OFF -- the \"\n \"scraper no-ops and does zero network I/O until an operator opts in \"\n \"(live scrape is operator-gated).\"\n ),\n )\n scrape_timeout: float = Field(\n 2.0,\n ge=0.1,\n le=30.0,\n description=(\n \"Per-scrape HTTP GET timeout in seconds. An engine that does not \"\n \"answer within this budget yields an empty (reachable=False) \"\n \"snapshot, never an exception.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n api_surfaces: ApiSurfacesSettings = Field(\n default_factory=ApiSurfacesSettings, # type: ignore[arg-type]\n description=(\n \"First-class API surfaces, persisted spend, and self-service key \"\n \"settings (cluster N: RouteIQ-e48a / 67fe / 3215; all default on / \"\n \"byte-stable).\"\n ),\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n bedrock_levers: BedrockRequestLeversSettings = Field(\n default_factory=BedrockRequestLeversSettings, # type: ignore[arg-type]\n description=(\n \"Per-request Bedrock Converse levers (requestMetadata / team \"\n \"callbacks / cachePoint) on the pre-call mutation seam (default off).\"\n ),\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n region_routing: RegionRoutingSettings = Field(\n default_factory=RegionRoutingSettings, # type: ignore[arg-type]\n description=(\n \"Per-request region-aware / data-residency candidate-filter settings \"\n \"(default off / identity).\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n engine_metrics: EngineMetricsSettings = Field(\n default_factory=EngineMetricsSettings, # type: ignore[arg-type]\n description=(\n \"Self-hosted-engine /metrics scrape settings (vLLM / Production \"\n \"Stack / AIBrix / llm-d queue-depth + KV-cache gauges; default off).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "4e4626b9689ccc8c1e7418b2790ee57ab4a2aaba": {"t": 1785769077.6688652, "n": 104032, "h": "51f70e110ec6a686", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H).\n\n Bundles the three MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), and shadow/mirror canary traffic (RouteIQ-4fd1). All three\n are independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass BedrockRequestLeversSettings(BaseModel):\n \"\"\"Per-request Bedrock Converse levers (data plane), default-OFF.\n\n Drives :class:`litellm_llmrouter.gateway.plugins.bedrock_request_levers\n .BedrockRequestLeversPlugin`, which mutates the outbound request on the\n correct litellm seam (``CustomLogger.async_pre_call_deployment_hook``) so\n the mutations actually reach the Bedrock ``Converse`` call args.\n\n Three independently-gated levers (each a byte-stable no-op when off):\n\n - ``request_metadata`` (RouteIQ-294a): forward tenant identity\n (workspace / key) as Bedrock ``requestMetadata`` tags for cost\n attribution. Lands as the top-level ``requestMetadata`` completion kwarg,\n which litellm's converse transform maps from ``non_default_params``.\n - ``team_callbacks`` (RouteIQ-9cd8): set per-team logging sinks as the\n TOP-LEVEL request kwargs ``callbacks`` / ``success_callback`` /\n ``failure_callback`` (where litellm sources dynamic per-request\n callbacks), NOT ``metadata[callbacks]`` (never read). The global\n registration is unaffected.\n - ``cache_point`` (RouteIQ-b9ee): drive Bedrock prompt-caching via\n litellm's ``cache_control_injection_points`` mechanism (system-prefix\n message injection + tool_config cachePoint), NOT a bare ``tools[]``\n ``cachePoint`` entry (dropped by the converse transform).\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_BEDROCK_LEVERS__REQUEST_METADATA``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACKS``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_POINT``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_SYSTEM``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_TOOLS``,\n ``ROUTEIQ_BEDROCK_LEVERS__METADATA_PREFIX``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACK_MAP`` (JSON\n ``{\"team-a\": {\"success_callback\": [\"s3\"]}}``).\n \"\"\"\n\n request_metadata: bool = Field(\n False,\n description=(\n \"RouteIQ-294a: forward tenant identity (workspace_id / key_id) as \"\n \"Bedrock requestMetadata tags for cost attribution. Default OFF -- \"\n \"byte-stable: no requestMetadata kwarg is added until enabled.\"\n ),\n )\n metadata_prefix: str = Field(\n \"routeiq_\",\n description=(\n \"Key prefix for the injected requestMetadata tags (Bedrock keys must \"\n \"match [a-zA-Z0-9 :_@$#=/+,.-]). Only used when request_metadata=True.\"\n ),\n )\n team_callbacks: bool = Field(\n False,\n description=(\n \"RouteIQ-9cd8: set per-team logging sinks as TOP-LEVEL request kwargs \"\n \"callbacks/success_callback/failure_callback (the seam litellm reads \"\n \"dynamic per-request callbacks from). Default OFF; the global \"\n \"registration is untouched.\"\n ),\n )\n team_callback_map: dict[str, dict[str, list[str]]] = Field(\n default_factory=dict,\n description=(\n \"Map of team_id -> {success_callback|failure_callback|callbacks: \"\n '[sink names]}. e.g. {\"team-a\": {\"success_callback\": [\"s3\"]}}. Only '\n \"applied when team_callbacks=True and the request resolves to a \"\n \"mapped team.\"\n ),\n )\n cache_point: bool = Field(\n False,\n description=(\n \"RouteIQ-b9ee: enable Bedrock prompt-caching cachePoint injection via \"\n \"litellm cache_control_injection_points. Default OFF -- byte-stable.\"\n ),\n )\n cache_system: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a system-prefix cachePoint (message \"\n \"injection point, role=system). Default on.\"\n ),\n )\n cache_tools: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a tool_config cachePoint so the tool \"\n \"schema prefix is cached. Default on. Only takes effect when the \"\n \"request carries tools.\"\n ),\n )\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\nclass EngineMetricsSettings(BaseModel):\n \"\"\"Self-hosted-engine ``/metrics`` scrape configuration (default OFF).\n\n Drives :mod:`litellm_llmrouter.engine_metrics`, the credless scraper that\n GETs a self-hosted inference engine's Prometheus ``/metrics`` (vLLM, vLLM\n Production Stack, AIBrix, llm-d -- all serve the ``vllm:*`` family) and\n parses the queue-depth / KV-cache-pressure gauges\n (``vllm:num_requests_waiting``, ``vllm:kv_cache_usage_perc`` /\n ``vllm:gpu_cache_usage_perc``, ...). Those signals feed a *future*\n KV/queue-aware Layer-1 router or an autoscaler-into-the-engine.\n\n LAYERING: this reads the engine *frontend's* aggregate ``/metrics`` so\n Layer-1 can decide whether to send more load to this engine arm; it never\n scrapes individual worker pods (that would collapse the two-layer model --\n see ``docs/architecture/aws-rearchitecture/51-multinode-large-model-serving.md``\n Part 3).\n\n Default DISABLED (opt-in): with ``enabled=False`` the scraper is a byte-stable\n no-op -- importing the module does no I/O, and ``scrape()`` returns an empty\n ``reachable=False`` snapshot without touching the network. Live scraping is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_ENGINE_METRICS__ENABLED``,\n ``ROUTEIQ_ENGINE_METRICS__SCRAPE_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the self-hosted-engine /metrics scrape. Default OFF -- the \"\n \"scraper no-ops and does zero network I/O until an operator opts in \"\n \"(live scrape is operator-gated).\"\n ),\n )\n scrape_timeout: float = Field(\n 2.0,\n ge=0.1,\n le=30.0,\n description=(\n \"Per-scrape HTTP GET timeout in seconds. An engine that does not \"\n \"answer within this budget yields an empty (reachable=False) \"\n \"snapshot, never an exception.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n bedrock_levers: BedrockRequestLeversSettings = Field(\n default_factory=BedrockRequestLeversSettings, # type: ignore[arg-type]\n description=(\n \"Per-request Bedrock Converse levers (requestMetadata / team \"\n \"callbacks / cachePoint) on the pre-call mutation seam (default off).\"\n ),\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n engine_metrics: EngineMetricsSettings = Field(\n default_factory=EngineMetricsSettings, # type: ignore[arg-type]\n description=(\n \"Self-hosted-engine /metrics scrape settings (vLLM / Production \"\n \"Stack / AIBrix / llm-d queue-depth + KV-cache gauges; default off).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "e9f18a5813a79b6c98de79dae146556ca1d03760": {"t": 1785769081.4034495, "n": 101715, "h": "6eef00a7717c6dd4", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H).\n\n Bundles the three MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), and shadow/mirror canary traffic (RouteIQ-4fd1). All three\n are independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass BedrockRequestLeversSettings(BaseModel):\n \"\"\"Per-request Bedrock Converse levers (data plane), default-OFF.\n\n Drives :class:`litellm_llmrouter.gateway.plugins.bedrock_request_levers\n .BedrockRequestLeversPlugin`, which mutates the outbound request on the\n correct litellm seam (``CustomLogger.async_pre_call_deployment_hook``) so\n the mutations actually reach the Bedrock ``Converse`` call args.\n\n Three independently-gated levers (each a byte-stable no-op when off):\n\n - ``request_metadata`` (RouteIQ-294a): forward tenant identity\n (workspace / key) as Bedrock ``requestMetadata`` tags for cost\n attribution. Lands as the top-level ``requestMetadata`` completion kwarg,\n which litellm's converse transform maps from ``non_default_params``.\n - ``team_callbacks`` (RouteIQ-9cd8): set per-team logging sinks as the\n TOP-LEVEL request kwargs ``callbacks`` / ``success_callback`` /\n ``failure_callback`` (where litellm sources dynamic per-request\n callbacks), NOT ``metadata[callbacks]`` (never read). The global\n registration is unaffected.\n - ``cache_point`` (RouteIQ-b9ee): drive Bedrock prompt-caching via\n litellm's ``cache_control_injection_points`` mechanism (system-prefix\n message injection + tool_config cachePoint), NOT a bare ``tools[]``\n ``cachePoint`` entry (dropped by the converse transform).\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_BEDROCK_LEVERS__REQUEST_METADATA``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACKS``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_POINT``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_SYSTEM``,\n ``ROUTEIQ_BEDROCK_LEVERS__CACHE_TOOLS``,\n ``ROUTEIQ_BEDROCK_LEVERS__METADATA_PREFIX``,\n ``ROUTEIQ_BEDROCK_LEVERS__TEAM_CALLBACK_MAP`` (JSON\n ``{\"team-a\": {\"success_callback\": [\"s3\"]}}``).\n \"\"\"\n\n request_metadata: bool = Field(\n False,\n description=(\n \"RouteIQ-294a: forward tenant identity (workspace_id / key_id) as \"\n \"Bedrock requestMetadata tags for cost attribution. Default OFF -- \"\n \"byte-stable: no requestMetadata kwarg is added until enabled.\"\n ),\n )\n metadata_prefix: str = Field(\n \"routeiq_\",\n description=(\n \"Key prefix for the injected requestMetadata tags (Bedrock keys must \"\n \"match [a-zA-Z0-9 :_@$#=/+,.-]). Only used when request_metadata=True.\"\n ),\n )\n team_callbacks: bool = Field(\n False,\n description=(\n \"RouteIQ-9cd8: set per-team logging sinks as TOP-LEVEL request kwargs \"\n \"callbacks/success_callback/failure_callback (the seam litellm reads \"\n \"dynamic per-request callbacks from). Default OFF; the global \"\n \"registration is untouched.\"\n ),\n )\n team_callback_map: dict[str, dict[str, list[str]]] = Field(\n default_factory=dict,\n description=(\n \"Map of team_id -> {success_callback|failure_callback|callbacks: \"\n '[sink names]}. e.g. {\"team-a\": {\"success_callback\": [\"s3\"]}}. Only '\n \"applied when team_callbacks=True and the request resolves to a \"\n \"mapped team.\"\n ),\n )\n cache_point: bool = Field(\n False,\n description=(\n \"RouteIQ-b9ee: enable Bedrock prompt-caching cachePoint injection via \"\n \"litellm cache_control_injection_points. Default OFF -- byte-stable.\"\n ),\n )\n cache_system: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a system-prefix cachePoint (message \"\n \"injection point, role=system). Default on.\"\n ),\n )\n cache_tools: bool = Field(\n True,\n description=(\n \"When cache_point=True, inject a tool_config cachePoint so the tool \"\n \"schema prefix is cached. Default on. Only takes effect when the \"\n \"request carries tools.\"\n ),\n )\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n bedrock_levers: BedrockRequestLeversSettings = Field(\n default_factory=BedrockRequestLeversSettings, # type: ignore[arg-type]\n description=(\n \"Per-request Bedrock Converse levers (requestMetadata / team \"\n \"callbacks / cachePoint) on the pre-call mutation seam (default off).\"\n ),\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "94923ff6493b21c9b3a76da0369e2a5b5734aa89": {"t": 1785769083.890741, "n": 97416, "h": "e0dd37218a7f0912", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n xray_enabled: bool = Field(\n False,\n description=(\n \"Emit AWS X-Ray-format trace IDs and propagate the \"\n \"``X-Amzn-Trace-Id`` header (RouteIQ-3c0a). When True, a NEW OTel \"\n \"TracerProvider is constructed with the AWS X-Ray ``id_generator`` \"\n \"(so spans carry X-Ray-format trace IDs) and the AWS X-Ray \"\n \"propagator is installed as the global text-map, giving CloudWatch \"\n \"Transaction Search / X-Ray a single edge-to-Bedrock trace. Default \"\n \"OFF so trace IDs stay W3C-format unless an operator opts in. \"\n \"Requires the optional ``opentelemetry-sdk-extension-aws`` + \"\n \"``opentelemetry-propagator-aws-xray`` packages (the ``otel`` \"\n \"extra); their absence degrades to W3C with a warning (never breaks \"\n \"boot). The X-Ray ID generator only applies when RouteIQ creates the \"\n \"provider - it cannot retrofit an existing reused SDK provider \"\n \"(env: ROUTEIQ_OTEL__XRAY_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n True,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. \"\n \"DECISION (RouteIQ-3b4d): default ON. The intelligent-routing use \"\n \"case (Claude Code -> RouteIQ -> mixed-Bedrock auto-group) is only \"\n \"worth it if the bandit LEARNS which arm is best automatically, so \"\n \"the loop closes by default. It is a NO-OP unless (a) a continuous \"\n \"learning strategy is registered (e.g. the Kumaraswamy-Thompson \"\n \"bandit -- itself off by default) AND (b) the eval pipeline is \"\n \"enabled; with both off this flag costs nothing. OPT-OUT: set \"\n \"``ROUTEIQ_ADAPTER_FRAMEWORK__MLOPS_FEEDBACK_LOOP=false``. \"\n \"JUDGE-COST NOTE: when the eval pipeline's LLM-as-judge evaluator is \"\n \"enabled it makes one extra judge call per sampled decision -- size \"\n \"the judge model / sample rate accordingly (see \"\n \"docs/operations/claude-code-routing.md).\"\n ),\n )\n\n\nclass MLOpsPromotionSettings(BaseModel):\n \"\"\"Quality-gated champion/challenger promotion + rollback (RouteIQ-2a1c).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ChampionChallengerPromoter`.\n When enabled, the promoter compares the aggregated routing quality of a\n designated *challenger* strategy/version against the current *champion* and\n PROMOTES the challenger (``registry.set_active``) when it beats the champion\n by ``margin`` over a window of at least ``min_samples`` evaluations. If a\n freshly promoted challenger later REGRESSES below the prior champion by the\n same margin, it is ROLLED BACK to the champion.\n\n The quality signal is the eval loop's ``{strategy: quality}`` aggregate\n (``ModelQualityTracker`` scale, ``[0, 1]``). Insufficient samples => no\n action (never promote on noise). Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable quality-gated champion/challenger promotion + rollback.\",\n )\n margin: float = Field(\n 0.05,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum quality lead (challenger - champion, in [0,1]) required to \"\n \"promote; the same drop below champion triggers a rollback.\"\n ),\n )\n min_samples: int = Field(\n 20,\n ge=1,\n description=(\n \"Minimum aggregated quality samples for BOTH champion and challenger \"\n \"before any promotion/rollback decision is made.\"\n ),\n )\n\n\nclass MLOpsDriftSettings(BaseModel):\n \"\"\"Input drift + routing-quality-regression detection (RouteIQ-6dce).\n\n Drives :class:`litellm_llmrouter.mlops.drift.DriftDetector`. Detects (a)\n INPUT DRIFT -- a shift in the request/bucket distribution vs a captured\n baseline (population-stability-index over coarse buckets) -- and (b)\n ROUTING-QUALITY REGRESSION -- the aggregated quality dropping below a\n captured baseline by ``quality_regression_threshold``. Drift signals are\n emitted as OTel gauges via ``metrics.py`` so CloudWatch / Prometheus can\n alarm. Default OFF.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable input-drift + quality-regression detection.\",\n )\n input_drift_threshold: float = Field(\n 0.2,\n ge=0.0,\n description=(\n \"Population-Stability-Index threshold above which the input \"\n \"(bucket) distribution counts as drifted (PSI>=0.2 is the textbook \"\n \"'moderate shift' line).\"\n ),\n )\n quality_regression_threshold: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=(\n \"Absolute aggregated-quality drop (baseline - current, in [0,1]) \"\n \"above which a routing-quality regression signal fires.\"\n ),\n )\n min_samples: int = Field(\n 30,\n ge=1,\n description=(\n \"Minimum observations in BOTH the baseline and current windows \"\n \"before drift/regression is evaluated (avoids small-sample noise).\"\n ),\n )\n window_size: int = Field(\n 200,\n ge=1,\n description=\"Sliding-window size for the current bucket/quality samples.\",\n )\n\n\nclass MLOpsShadowSettings(BaseModel):\n \"\"\"Shadow / mirror (silent canary) traffic to candidate strategies\n (RouteIQ-4fd1).\n\n Drives :class:`litellm_llmrouter.strategy_registry.ShadowMirror`. Mirrors a\n sampled fraction of routing decisions to a candidate strategy WITHOUT\n affecting the served response: it computes what the candidate WOULD have\n picked and records it for offline comparison. No user impact. Default OFF\n with ``sample_rate=0.0``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable shadow/mirror evaluation of a candidate strategy.\",\n )\n candidate_strategy: str = Field(\n \"\",\n description=(\n \"Registry name of the candidate strategy to shadow-evaluate. Empty \"\n \"disables mirroring even when enabled.\"\n ),\n )\n sample_rate: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=\"Fraction of served decisions to mirror to the candidate.\",\n )\n max_records: int = Field(\n 1000,\n ge=1,\n description=\"Bounded ring buffer of shadow comparison records to retain.\",\n )\n\n\nclass MLOpsSettings(BaseModel):\n \"\"\"MLOps closed-loop configuration (Cluster H).\n\n Bundles the three MLOps sub-loops that keep routing strategies optimal:\n quality-gated champion/challenger promotion (RouteIQ-2a1c), drift detection\n (RouteIQ-6dce), and shadow/mirror canary traffic (RouteIQ-4fd1). All three\n are independently settings-gated and DEFAULT OFF -- importing or wiring the\n MLOps machinery changes nothing until an operator opts in.\n\n Builds on the EXISTING COLLECT/EVALUATE/AGGREGATE/FEEDBACK eval loop\n (``ModelQualityTracker`` / ``MLOpsCoordinator``); it does not duplicate it.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_MLOPS__PROMOTION__ENABLED``,\n ``ROUTEIQ_MLOPS__DRIFT__ENABLED``,\n ``ROUTEIQ_MLOPS__SHADOW__ENABLED``, etc.\n \"\"\"\n\n promotion: MLOpsPromotionSettings = Field(\n default_factory=MLOpsPromotionSettings, # type: ignore[arg-type]\n description=\"Quality-gated champion/challenger promotion + rollback.\",\n )\n drift: MLOpsDriftSettings = Field(\n default_factory=MLOpsDriftSettings, # type: ignore[arg-type]\n description=\"Input-drift + routing-quality-regression detection.\",\n )\n shadow: MLOpsShadowSettings = Field(\n default_factory=MLOpsShadowSettings, # type: ignore[arg-type]\n description=\"Shadow/mirror (silent canary) traffic to candidate strategies.\",\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n auto_group: bool = Field(\n False,\n description=(\n \"Synthesize a SINGLE mixed-Bedrock routing GROUP from discovery: \"\n \"every discovered serverless arm (Claude + Nova + gpt-oss + any \"\n \"future provider) shares one ``model_name`` (``auto_group_name``) so \"\n \"a routing strategy (e.g. the Kumaraswamy-Thompson bandit) cascades \"\n \"across the arms instead of the caller pinning one tier. This powers \"\n \"the Claude-Code cost-routing recipe (point ``model`` at the group \"\n \"name). Default OFF -- byte-stable: when off the per-model distinct \"\n \"``model_name`` mapping is unchanged. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP``.\"\n ),\n )\n auto_group_name: str = Field(\n \"claude-auto\",\n description=(\n \"The shared ``model_name`` for the synthesized auto-group (only used \"\n \"when ``auto_group=True``). Point an unmodified Anthropic client's \"\n \"``model`` at this value (or rewrite to it via the model-alias layer) \"\n \"to route through the mixed-Bedrock group. Env: \"\n \"``ROUTEIQ_BEDROCK_DISCOVERY__AUTO_GROUP_NAME``.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass ModelAliasSettings(BaseModel):\n \"\"\"Pre-routing model-name alias/rewrite map (RouteIQ-0dcb).\n\n Claude Code (and any unmodified Anthropic SDK client) PINS a concrete model\n id in the request body, e.g. ``claude-sonnet-4-20250514``. LiteLLM matches\n ``model_list`` rows by EXACT ``model_name``, so a pinned id never lands on a\n synthesized routing group such as ``claude-auto`` -- the request would 400 or\n fall through to a single arm.\n\n This layer rewrites the request ``model`` field BEFORE the request reaches\n LiteLLM, at the RouteIQ app/route entry layer (a raw-ASGI middleware in\n ``model_alias.py``, NOT the broken ``on_llm_pre_call`` mutation seam). Two\n maps are applied in order:\n\n * ``exact`` -- an exact ``{requested_id: target_group}`` lookup (fast path).\n * ``regex`` -- ordered ``{pattern: target_group}`` rules; the FIRST full\n ``re.fullmatch`` wins. Patterns are compiled once; an invalid pattern is\n logged and skipped (fail-open -- a bad rule never blocks traffic).\n\n Default ``enabled=False`` with empty maps => IDENTITY (no rewrite), so the\n behavior is byte-stable until an operator opts in.\n\n Env vars (``__`` nested delimiter under ``ROUTEIQ_``):\n ``ROUTEIQ_MODEL_ALIAS__ENABLED``,\n ``ROUTEIQ_MODEL_ALIAS__EXACT`` (JSON object),\n ``ROUTEIQ_MODEL_ALIAS__REGEX`` (JSON object of ordered rules).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable the pre-routing model-alias rewrite layer. Default OFF \"\n \"(identity -- no rewrite). When on, the request ``model`` field is \"\n \"rewritten via ``exact`` then ``regex`` before LiteLLM sees it, so an \"\n \"unmodified Anthropic client pinning a concrete id is transparently \"\n \"routed through a target group (e.g. ``claude-auto``).\"\n ),\n )\n exact: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Exact-match alias map ``{requested_model_id: target_group}``. \"\n \"Applied first; a hit short-circuits the regex pass. Env \"\n \"``ROUTEIQ_MODEL_ALIAS__EXACT`` takes a JSON object, e.g. \"\n '``{\"claude-sonnet-4-20250514\": \"claude-auto\"}``.'\n ),\n )\n regex: dict[str, str] = Field(\n default_factory=dict,\n description=(\n \"Ordered regex alias rules ``{pattern: target_group}``; the FIRST \"\n \"rule whose pattern ``re.fullmatch`` matches the requested id wins. \"\n \"Env ``ROUTEIQ_MODEL_ALIAS__REGEX`` takes a JSON object, e.g. \"\n '``{\"^claude-.*$\": \"claude-auto\"}``. Dict insertion order is the '\n \"evaluation order (Python 3.7+). An uncompilable pattern is logged \"\n \"and skipped (fail-open).\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n model_alias: ModelAliasSettings = Field(\n default_factory=ModelAliasSettings, # type: ignore[arg-type]\n description=(\n \"Pre-routing model-name alias/rewrite map (default off / identity).\"\n ),\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n mlops: MLOpsSettings = Field(\n default_factory=MLOpsSettings, # type: ignore[arg-type]\n description=(\n \"MLOps closed-loop settings: quality-gated promotion, drift \"\n \"detection, and shadow/mirror canary (all default off).\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "58083d15137a5429cc4d88dd4da56f0340caaaf6": {"t": 1785769085.3870373, "n": 85121, "h": "f43fe005c39c44bb", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n cluster_mode: bool = Field(\n False,\n description=(\n \"Build a ``redis.asyncio.RedisCluster`` / sync ``redis.RedisCluster`` \"\n \"client instead of a single-endpoint ``Redis`` (RouteIQ-5d8f). For \"\n \"ElastiCache cluster-mode-enabled / Redis Cluster deployments where the \"\n \"host is the cluster configuration endpoint. The static-cred + IAM-auth \"\n \"(SigV4 elasticache:Connect) paths both apply to the cluster client. \"\n \"Env: ROUTEIQ_REDIS__CLUSTER_MODE. Default OFF -> the single-endpoint \"\n \"path is byte-for-byte unchanged.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n reader_host: Optional[str] = Field(\n None,\n description=(\n \"Optional read-only endpoint host (Aurora reader endpoint / RDS Proxy \"\n \"read endpoint) for read replica routing (RouteIQ-65ed). When set, a \"\n \"SEPARATE read pool is built against this host (same port/db/user/IAM \"\n \"discipline as the writer) and used for read-only queries. Default \"\n \"empty -> reads go to the writer pool, so the single-pool path is \"\n \"byte-for-byte unchanged. Env: ROUTEIQ_POSTGRES__READER_HOST.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass TagRoutingSettings(BaseModel):\n \"\"\"Tag / regex / User-Agent routing strategy configuration (RouteIQ-6865).\n\n Upstream LiteLLM ``tag_based_routing`` is BYPASSED by the RouteIQ custom\n routing strategy (``set_custom_routing_strategy``), so RouteIQ re-implements\n tag routing natively. A request is matched against three signal sources, in\n priority order, and the FIRST matching rule selects a model-group subset:\n\n 1. An explicit request **tag** (``context.request_kwargs[\"tags\"]`` /\n ``context.metadata[\"tags\"]`` -- list or scalar) matched against\n ``tag_model_groups`` keys (exact).\n 2. A **regex** over the request text (last user message / input) matched\n against ``regex_model_groups`` keys (each key is a Python regex).\n 3. The **User-Agent** header (``context.request_kwargs[\"headers\"]`` /\n ``context.metadata[\"headers\"]``, case-insensitive) matched against\n ``user_agent_model_groups`` keys (substring, case-insensitive).\n\n Each value is an ordered list of model-name patterns; the request is routed\n to the first candidate whose ``litellm_params.model`` matches one pattern\n (exact first, then substring). No match (or no candidate in the matched\n group) falls back to the first available candidate -- never ``None`` unless\n the group is empty.\n\n Env vars: ``ROUTEIQ_TAG_ROUTING__ENABLED``,\n ``ROUTEIQ_TAG_ROUTING__TAG_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__REGEX_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_TAG_ROUTING__USER_AGENT_MODEL_GROUPS`` (JSON map) (``__`` delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the tag/regex/User-Agent routing strategy at startup.\",\n )\n tag_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of request tag -> ordered list of model-name patterns. A tag in \"\n \"``request_kwargs['tags']`` / ``metadata['tags']`` dispatches to the \"\n 'first matching candidate in its group (e.g. {\"premium\": [\"gpt-4o\"]}).'\n ),\n )\n regex_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of Python regex -> ordered list of model-name patterns. The \"\n \"request text is matched against each key; the first matching regex \"\n \"dispatches to its group. Consulted only when no tag matched.\"\n ),\n )\n user_agent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of User-Agent substring (case-insensitive) -> ordered list of \"\n \"model-name patterns. Matched against the request User-Agent header. \"\n \"Consulted only when neither a tag nor a regex matched.\"\n ),\n )\n\n\nclass LatencyAwareSettings(BaseModel):\n \"\"\"Latency-SLA / usage-based / least-busy routing strategy configuration\n (RouteIQ-904b).\n\n Upstream LiteLLM's ``latency-based-routing`` / ``usage-based-routing`` /\n ``least-busy`` strategies are BYPASSED by RouteIQ's custom routing strategy\n (``set_custom_routing_strategy``). These three RouteIQ-native strategies\n re-implement that family, reading LIVE signals from the per-worker\n :class:`~litellm_llmrouter.router_decision_callback.RoutingStatsAccumulator`\n (decision counts + rolling latency) and a per-strategy in-flight counter,\n and degrade gracefully (deterministic fallback) when no signal exists yet.\n\n All three share this one settings block (gated by ``enabled``); each is\n independently registry-registered under its own name:\n ``llmrouter-latency-sla`` / ``llmrouter-usage-based`` /\n ``llmrouter-least-busy``.\n\n Env vars: ``ROUTEIQ_LATENCY_AWARE__ENABLED``,\n ``ROUTEIQ_LATENCY_AWARE__P_LATENCY_TARGET_MS``,\n ``ROUTEIQ_LATENCY_AWARE__PERCENTILE`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Register the latency-SLA / usage-based / least-busy strategies \"\n \"at startup (all three, each under its own registry name).\"\n ),\n )\n p_latency_target_ms: float = Field(\n 2000.0,\n ge=0.0,\n description=(\n \"Latency-SLA target (milliseconds) at the configured percentile. The \"\n \"latency-SLA strategy prefers the cheapest/first deployment whose \"\n \"observed percentile latency is at or below this target; if none \"\n \"meet it, it picks the lowest-latency deployment (best effort).\"\n ),\n )\n percentile: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=(\n \"Latency percentile the SLA target is evaluated at (e.g. 0.95 = p95). \"\n \"Computed from the per-model latency samples held by the strategy.\"\n ),\n )\n max_latency_samples: int = Field(\n 256,\n ge=1,\n le=4096,\n description=(\n \"Per-model ring-buffer size for latency samples used to estimate the \"\n \"percentile. Bounded so memory stays constant on the hot path.\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass LinUCBSettings(BaseModel):\n \"\"\"Feature-vector contextual-bandit (LinUCB) routing strategy configuration\n (RouteIQ-6c67).\n\n The Kumaraswamy-Thompson bandit is BUCKET-contextual (one posterior per\n coarse ``(task_bucket, arm)`` pair). LinUCB is FEATURE-VECTOR contextual: it\n learns a linear reward model over a real-valued context vector (prompt-length\n feature, requested-profile one-hot, tenant hash bucket) and scores each arm\n by its UCB ``theta·x + alpha·sqrt(x^T A^-1 x)`` -- the second term drives\n cold-start exploration that shrinks as evidence accumulates.\n\n ADDITIVE alongside the KT bandit: it is a SEPARATE registry strategy\n (``llmrouter-linucb``), gated independently by ``enabled``. Pure stdlib\n (no numpy): per-arm state is the incremental ``A^-1`` maintained via\n rank-1 Sherman-Morrison updates, matching the KT module's numpy-free\n discipline. The RNG (used only for tie-breaking) is threaded as a\n ``random.Random`` object.\n\n Env vars: ``ROUTEIQ_LINUCB__ENABLED``, ``ROUTEIQ_LINUCB__ALPHA``,\n ``ROUTEIQ_LINUCB__TENANT_BUCKETS``, ``ROUTEIQ_LINUCB__SEED`` (``__`` delim).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the LinUCB contextual-bandit strategy at startup.\",\n )\n alpha: float = Field(\n 1.0,\n ge=0.0,\n le=10.0,\n description=(\n \"Exploration coefficient on the UCB confidence width. Higher = more \"\n \"exploration; 0 = pure greedy exploit. Default 1.0 (textbook LinUCB).\"\n ),\n )\n tenant_buckets: int = Field(\n 8,\n ge=1,\n le=256,\n description=(\n \"Number of hashed tenant feature buckets in the context vector. \"\n \"Bounds the feature dimension so per-arm matrices stay small.\"\n ),\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic tie-breaking in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n tag_routing: TagRoutingSettings = Field(\n default_factory=TagRoutingSettings, # type: ignore[arg-type]\n description=\"Tag / regex / User-Agent routing strategy settings.\",\n )\n latency_aware: LatencyAwareSettings = Field(\n default_factory=LatencyAwareSettings, # type: ignore[arg-type]\n description=(\n \"Latency-SLA / usage-based / least-busy routing strategy settings.\"\n ),\n )\n linucb: LinUCBSettings = Field(\n default_factory=LinUCBSettings, # type: ignore[arg-type]\n description=\"LinUCB feature-vector contextual-bandit routing settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "21ad251cc7a8e6f982a556bfbf33707ba52eb0cb": {"t": 1785769088.4144924, "n": 76189, "h": "08d2d1d9ad675e95", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n#: Nested BaseModel fields whose DOCUMENTED flat env var is a bare bool that\n#: should map onto the sub-model's ``enabled`` flag (RouteIQ-778e). The flat\n#: ``ROUTEIQ_EVAL_PIPELINE=true`` collides with the nested ``eval_pipeline``\n#: BaseModel field: pydantic-settings JSON-decodes the bool ``true`` and then\n#: tries to validate it as an ``EvalPipelineSettings`` model, raising\n#: ``ValidationError`` and aborting ALL of ``GatewaySettings()``. We expand the\n#: bare bool to ``{\"enabled\": <bool>}`` at the decode point so the documented\n#: enable var constructs cleanly (the ``__``-nested forms such as\n#: ``ROUTEIQ_EVAL_PIPELINE__ENABLED=true`` are handled by pydantic natively and\n#: are unaffected).\n_FLAT_BOOL_MODEL_ENV_FIELDS = frozenset({\"eval_pipeline\"})\n\n# Bare-bool string spellings accepted for a flat model-enable env var.\n_TRUE_BOOL_STRINGS = frozenset({\"true\", \"1\", \"yes\", \"on\"})\n_FALSE_BOOL_STRINGS = frozenset({\"false\", \"0\", \"no\", \"off\", \"\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n\n For the fields in :data:`_FLAT_BOOL_MODEL_ENV_FIELDS` we accept a DOCUMENTED\n flat bool (e.g. ``ROUTEIQ_EVAL_PIPELINE=true``) and expand it onto the\n sub-model's ``enabled`` flag, so the bare bool does not crash\n ``GatewaySettings()`` against the nested BaseModel (RouteIQ-778e).\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n if field_name in _FLAT_BOOL_MODEL_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON object (the\n # ``{\"enabled\": true, ...}`` form, or a ``__``-nested expansion);\n # only the DOCUMENTED bare bool needs the shim.\n if not stripped.startswith(\"{\"):\n lowered = stripped.lower()\n if lowered in _TRUE_BOOL_STRINGS:\n return {\"enabled\": True}\n if lowered in _FALSE_BOOL_STRINGS:\n return {\"enabled\": False}\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "06ac06a1953e1756bc4714780251030529908a12": {"t": 1785769090.2367156, "n": 74328, "h": "2a2876b1af61fd75", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass CostCascadeSettings(BaseModel):\n \"\"\"Cost-aware speculative-cascade routing strategy configuration (RouteIQ-90d0).\n\n Distinct from :class:`CostAwareRoutingStrategy` (heuristic Pareto, single-shot):\n the cascade orders deployments cheapest -> strongest and routes to the\n CHEAPEST capable rung first. Each selection exposes the ordered escalation\n ladder in ``deployment[\"metadata\"][\"routeiq_cascade\"]`` so the caller can\n retry UP the ladder on a low-quality/low-confidence signal (mode (a)). When\n a prior-attempt confidence/rung signal is present in\n ``context.request_kwargs`` / ``context.metadata`` (keys ``cascade_rung`` or\n ``cascade_confidence``), the strategy advances to the NEXT rung itself\n (mode (b), confidence-gated).\n\n Costs are read from ``litellm.model_cost``; arms with unknown cost sort LAST\n (treated as most expensive) so the cheap-first invariant degrades gracefully.\n\n Env vars: ``ROUTEIQ_COST_CASCADE__ENABLED``,\n ``ROUTEIQ_COST_CASCADE__CONFIDENCE_THRESHOLD``,\n ``ROUTEIQ_COST_CASCADE__MAX_RUNGS`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the cost-aware cascade strategy at startup.\",\n )\n confidence_threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum prior-attempt confidence below which the cascade escalates \"\n \"to the next (pricier/stronger) rung. Only consulted in the \"\n \"confidence-gated mode (b) when a prior signal is present.\"\n ),\n )\n max_rungs: int = Field(\n 4,\n ge=1,\n le=32,\n description=(\n \"Maximum number of rungs to expose on the escalation ladder \"\n \"(cheapest -> strongest). Bounds the ladder length in the returned \"\n \"metadata and caps how far mode (b) can escalate.\"\n ),\n )\n\n\nclass SemanticIntentSettings(BaseModel):\n \"\"\"Semantic / embedding intent-router configuration (RouteIQ-7936).\n\n Routes by request INTENT -> model-group. Classifies the request via\n embedding similarity to per-intent centroids (reusing the shared\n SentenceTransformer encoder), then maps the winning intent to a configured\n model-group / deployment subset.\n\n HOT-PATH DISCIPLINE: the embedding model is NEVER cold-loaded on the request\n path. The strategy only classifies when the shared encoder is ALREADY\n loaded (mirrors the centroid_routing discipline); otherwise it falls through\n to a graceful fallback deployment. Pre-warm at startup to enable\n classification.\n\n Env vars: ``ROUTEIQ_SEMANTIC_INTENT__ENABLED``,\n ``ROUTEIQ_SEMANTIC_INTENT__INTENT_MODEL_GROUPS`` (JSON map),\n ``ROUTEIQ_SEMANTIC_INTENT__SIMILARITY_THRESHOLD`` (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the semantic intent router at startup.\",\n )\n intent_model_groups: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=(\n \"Map of intent label -> list of model name patterns that form the \"\n \"target model group for that intent (e.g. \"\n '``{\"code\": [\"gpt-4o\", \"claude-sonnet\"], \"chat\": [\"gpt-4o-mini\"]}``). '\n \"A request classified to an intent is routed to a deployment whose \"\n \"model matches one of the patterns; an unmapped intent falls back.\"\n ),\n )\n similarity_threshold: float = Field(\n 0.0,\n ge=0.0,\n le=1.0,\n description=(\n \"Minimum top-1 cosine similarity to accept an intent classification. \"\n \"Below this the request is treated as UNMAPPED and falls back.\"\n ),\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n cost_cascade: CostCascadeSettings = Field(\n default_factory=CostCascadeSettings, # type: ignore[arg-type]\n description=\"Cost-aware speculative-cascade routing strategy settings.\",\n )\n semantic_intent: SemanticIntentSettings = Field(\n default_factory=SemanticIntentSettings, # type: ignore[arg-type]\n description=\"Semantic / embedding intent-router strategy settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "ea3ef80aaff549020354f0ef293f8db196c0f140": {"t": 1785769091.8880377, "n": 70178, "h": "1aaf5d8382e8a52e", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic.fields import FieldInfo\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom pydantic_settings.sources import EnvSettingsSource\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass BedrockDiscoverySettings(BaseModel):\n \"\"\"In-process AWS Bedrock model auto-discovery configuration (control plane).\n\n Drives :mod:`litellm_llmrouter.bedrock_discovery`, which enumerates every\n provider's serverless foundation models and cross-region inference profiles\n across ``source_regions`` and maps them into LiteLLM ``model_list`` entries.\n\n The scan is a CONTROL-PLANE concern -- it uses ``boto3.client(\"bedrock\")``\n (NOT ``bedrock-runtime``) and only issues the three read-only control APIs\n ``ListFoundationModels`` / ``ListInferenceProfiles`` /\n ``GetFoundationModelAvailability``.\n\n Default DISABLED (opt-in): with ``enabled=False`` the discovery module is a\n byte-stable no-op, so importing it or calling ``discover_models()`` changes\n nothing until an operator turns it on. Live multi-region scanning is\n therefore operator-gated.\n\n Env vars (``__`` nested delimiter under the ``ROUTEIQ_`` prefix):\n ``ROUTEIQ_BEDROCK_DISCOVERY__ENABLED``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` (accepts a plain\n comma-separated string ``us-east-1,eu-west-1`` OR a JSON list\n ``[\"us-east-1\",\"eu-west-1\"]`` -- the CSV form is normalised at the env-source\n decode point by :class:`_RouteIQEnvSettingsSource`, the JSON form mirrors the\n ``governance.banned_models`` nested-list convention; either form constructs\n ``GatewaySettings()`` cleanly),\n ``ROUTEIQ_BEDROCK_DISCOVERY__CHECK_AVAILABILITY``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__RESIDENCY_CONSTRAINT``,\n ``ROUTEIQ_BEDROCK_DISCOVERY__REGISTER_COST``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=(\n \"Enable in-process Bedrock model auto-discovery. Default OFF -- the \"\n \"discovery module no-ops and the model_list is untouched until an \"\n \"operator opts in (live multi-region scan is operator-gated).\"\n ),\n )\n source_regions: list[str] = Field(\n default_factory=list,\n description=(\n \"AWS regions to scan for serverless foundation models + inference \"\n \"profiles. Intersected with get_available_regions('bedrock'). Empty \"\n \"falls back to the boto3 session default region (AWS_REGION / profile \"\n \"/ IMDS). Env ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS`` accepts \"\n \"EITHER a comma-separated string (e.g. ``us-east-1,eu-west-1``) OR a \"\n 'JSON list (e.g. ``[\"us-east-1\",\"eu-west-1\"]``); the CSV form is '\n \"normalised by the RouteIQ env source so neither crashes \"\n \"GatewaySettings().\"\n ),\n )\n check_availability: bool = Field(\n False,\n description=(\n \"Gate discovered models through GetFoundationModelAvailability \"\n \"(entitlement check). Default off -- the extra per-model control call \"\n \"is opt-in; when off all ACTIVE serverless models are kept.\"\n ),\n )\n residency_constraint: bool = Field(\n False,\n description=(\n \"When True the selector SKIPS the global.* inference-profile tier and \"\n \"prefers an in-geo geographic profile (or the raw modelId) so traffic \"\n \"stays in-region (data-residency guard, spec #11-12). Default off \"\n \"(global-first).\"\n ),\n )\n register_cost: bool = Field(\n True,\n description=(\n \"When mapping to LiteLLM model_list, register a cost-key stub for \"\n \"global.* ids (often missing from LiteLLM's model_prices JSON, which \"\n \"breaks provider detection -- issue #17286). Default on.\"\n ),\n )\n\n @field_validator(\"source_regions\", mode=\"before\")\n @classmethod\n def _split_csv(cls, v: Any) -> Any:\n \"\"\"Accept a comma-separated string OR a list for ``source_regions``.\n\n This handles DIRECT/programmatic construction\n (``BedrockDiscoverySettings(source_regions=\"us-east-1,eu-west-1\")``).\n The ENV form is normalised earlier, at the source level, by\n :class:`_RouteIQEnvSettingsSource` -- a field validator alone cannot fix\n the env path because pydantic-settings JSON-decodes the complex\n ``list[str]`` before any validator runs.\n \"\"\"\n if isinstance(v, str):\n return [r.strip() for r in v.split(\",\") if r.strip()]\n return v\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Custom env source -- comma-separated coercion for documented list fields\n# ============================================================================\n\n#: Nested env fields (``list[str]``) whose DOCUMENTED env form is a plain\n#: comma-separated string. pydantic-settings JSON-decodes complex types\n#: (``list[str]``) straight out of the env BEFORE any field/model validator\n#: runs, so a CSV string would raise ``SettingsError`` and abort the WHOLE of\n#: ``GatewaySettings()`` (not just the affected sub-model). We normalise the\n#: CSV form to a real list at the env-source decode point so both the\n#: comma-separated and the JSON-list env forms construct cleanly.\n_CSV_LIST_ENV_FIELDS = frozenset({\"source_regions\"})\n\n\nclass _RouteIQEnvSettingsSource(EnvSettingsSource):\n \"\"\"``EnvSettingsSource`` that accepts comma-separated lists for a few fields.\n\n The base source JSON-decodes every complex (``list[str]`` etc.) nested env\n var. For the fields in :data:`_CSV_LIST_ENV_FIELDS` we instead accept a\n plain comma-separated string (``a,b,c``) -- the documented env form -- and\n fall back to JSON for anything that parses as JSON (so a real JSON list\n ``[\"a\",\"b\"]`` still works). This is what makes\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1`` boot the\n gateway instead of crashing it.\n \"\"\"\n\n def decode_complex_value(\n self, field_name: str, field: FieldInfo, value: Any\n ) -> Any:\n if field_name in _CSV_LIST_ENV_FIELDS and isinstance(value, str):\n stripped = value.strip()\n # Defer to JSON when the operator gave a JSON list/scalar; only the\n # bare comma-separated form needs the shim.\n if not stripped.startswith((\"[\", '\"')):\n return [r.strip() for r in stripped.split(\",\") if r.strip()]\n return super().decode_complex_value(field_name, field, value)\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n bedrock_discovery: BedrockDiscoverySettings = Field(\n default_factory=BedrockDiscoverySettings, # type: ignore[arg-type]\n description=\"In-process Bedrock model auto-discovery settings (default off).\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n\n The default ``env_settings`` source is swapped for\n :class:`_RouteIQEnvSettingsSource` so the DOCUMENTED comma-separated env\n form of CSV-list fields (e.g.\n ``ROUTEIQ_BEDROCK_DISCOVERY__SOURCE_REGIONS=us-east-1,eu-west-1``) is\n accepted instead of crashing ``GatewaySettings()`` on the JSON decode.\n \"\"\"\n env_settings = _RouteIQEnvSettingsSource(settings_cls)\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "b819891fde2d119f83bf658f37283cd7ce152c96": {"t": 1785769093.4072258, "n": 63137, "h": "48997a997862e79e", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the byte-stable DEFAULT backend.\n\n DURABLE (RouteIQ-95a8): set ``backend='file'`` to select the cred-free,\n pure-stdlib ``FilePosteriorBackend`` — it mirrors the in-memory semantics but\n persists posteriors to ``state_path`` via an atomic ``json.dump`` +\n ``os.replace`` (the governance-store pattern) so a worker restart RESUMES\n convergence instead of starting cold. Persistence is debounced off the hot\n path; the prior posteriors are loaded at startup. The default stays\n ``memory`` for byte-stable behavior.\n\n RESERVED / not yet wired (no consumer): the ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"Posterior backend (memory | file). ``memory`` (default) is the \"\n \"byte-stable per-worker ``InMemoryPosteriorBackend``. ``file`` \"\n \"selects the cred-free DURABLE ``FilePosteriorBackend`` which \"\n \"persists posteriors to ``state_path`` (atomic json.dump + \"\n \"os.replace) and reloads them at startup so a restart resumes \"\n \"convergence (RouteIQ-95a8). The durable Redis/Aurora backends \"\n \"remain NOT built (RouteIQ-4654).\"\n ),\n )\n state_path: str = Field(\n \"\",\n description=(\n \"Filesystem path for the DURABLE ``file`` backend's posterior \"\n \"snapshot (RouteIQ-95a8). Only consulted when ``backend='file'``; \"\n \"empty disables persistence (the file backend then behaves like the \"\n \"in-memory one). Env: \"\n \"``ROUTEIQ_KUMARASWAMY_THOMPSON__STATE_PATH``.\"\n ),\n )\n flush_interval_seconds: float = Field(\n 5.0,\n ge=0.0,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): max seconds between debounced \"\n \"persists. 0 disables the time-based flush (count-based only).\"\n ),\n )\n flush_dirty_threshold: int = Field(\n 32,\n ge=1,\n description=(\n \"DURABLE ``file`` backend (RouteIQ-95a8): persist after this many \"\n \"posterior mutations accumulate (off-hot-path debounce). 1 = \"\n \"write-through.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "49f86b0d94ce8197371158775fa45a8bf0a41029": {"t": 1785769094.9278338, "n": 61470, "h": "8ff74b0be1e540ae", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps:\n ``InMemoryPosteriorBackend`` is the ONLY backend that exists.\n\n RESERVED / not yet wired (no consumer): the ``backend``, ``durable`` and\n ``cost_reward_alpha`` fields below have ZERO readers.\n ``register_kumaraswamy_thompson_strategy`` never reads them, and the durable\n Redis (hot) / Aurora (durable) posterior backends they would select are NOT\n built. The fields are retained for env-var forward-compat only (RouteIQ-4654;\n the module docstring in ``kumaraswamy_thompson.py`` documents the same).\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=(\n \"RESERVED / not yet wired (no consumer): hot posterior backend \"\n \"(memory | redis). Only the in-memory backend exists; the durable \"\n \"Redis posterior backend is NOT built and \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n durable: str = Field(\n \"none\",\n description=(\n \"RESERVED / not yet wired (no consumer): durable posterior store \"\n \"(none | aurora). The durable Aurora posterior backend is NOT built \"\n \"and ``register_kumaraswamy_thompson_strategy`` never reads this \"\n \"field (RouteIQ-4654). Retained for env-var forward-compat.\"\n ),\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=(\n \"RESERVED / not yet wired (no consumer): mixing rate of the cost \"\n \"term into the reward signal. \"\n \"``register_kumaraswamy_thompson_strategy`` never reads this field \"\n \"(the strategy uses ``w_quality`` / ``w_cost`` / ``w_latency`` \"\n \"instead) — RouteIQ-4654. Retained for env-var forward-compat.\"\n ),\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n validation_alias=\"LLMROUTER_GOVERNANCE_SPEND_TRACKING\",\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry. \"\n \"The ``validation_alias`` binds the bare env name (NOT the \"\n \"``ROUTEIQ_``-prefixed form), making this field track the historical \"\n \"env var so a future consumer can read it via ``get_settings()`` per \"\n \"ADR-0013. NOTE: the live reader ``router_decision_callback.\"\n \"_governance_spend_tracking_enabled()`` still reads ``os.getenv`` \"\n \"directly (RouteIQ-9f9f); the alias keeps the two in lock-step on the \"\n \"same env name + default until that consumer is migrated.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "82fb3ef412cbb6119934e163a8c9309e544cdd48": {"t": 1785769096.4584186, "n": 59539, "h": "ddbca85669b353a7", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps;\n Redis (hot) / Aurora (durable) backends are wired behind ``backend`` /\n ``durable`` flags for the live substrate (P1/P2), default off.\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=\"Hot posterior backend: memory | redis.\",\n )\n durable: str = Field(\n \"none\",\n description=\"Durable posterior store: none | aurora.\",\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Mixing rate of the cost term into the reward signal.\",\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n\n # Map the OTel-STANDARD endpoint env var onto otel.endpoint (RouteIQ-cfe3).\n # The whole deploy surface (entrypoints, docker-compose, every config/*.yaml,\n # the docs, service_discovery.py) uses OTEL_EXPORTER_OTLP_ENDPOINT, and\n # ADR-0013 explicitly migrated it -- but the nested field only binds the\n # ROUTEIQ_OTEL__ENDPOINT form (nobody sets it), so post-migration the\n # ObservabilityManager silently exported to localhost while service\n # discovery probed the real collector (split brain). pydantic-settings\n # won't resolve a flat env var onto a nested BaseModel field, so inject it\n # here. The nested ROUTEIQ_OTEL__ENDPOINT form WINS if both are set.\n otlp_endpoint = _os.environ.get(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n if otlp_endpoint:\n current = data.get(\"otel\")\n if current is None:\n data[\"otel\"] = {\"endpoint\": otlp_endpoint}\n elif isinstance(current, dict):\n current.setdefault(\"endpoint\", otlp_endpoint)\n elif isinstance(current, BaseModel):\n if getattr(current, \"endpoint\", None) is None:\n data[\"otel\"] = current.model_copy(\n update={\"endpoint\": otlp_endpoint}\n )\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "9f330df32c40564ed8a729de1a24bcc548306a79": {"t": 1785769097.9002109, "n": 58233, "h": "75be52b2ce610808", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps;\n Redis (hot) / Aurora (durable) backends are wired behind ``backend`` /\n ``durable`` flags for the live substrate (P1/P2), default off.\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=\"Hot posterior backend: memory | redis.\",\n )\n durable: str = Field(\n \"none\",\n description=\"Durable posterior store: none | aurora.\",\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Mixing rate of the cost term into the reward signal.\",\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 moment-fit Beta(alpha,beta) -> \"\n \"Kumaraswamy(a,b) mapping (matches mean + variance via a 1-D \"\n \"root-find) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the mean (~1e-8) and tracks the variance \"\n \"(~1.0x, vs the shortcut's distortion). Cached per-posterior on the \"\n \"exact (alpha,beta) so any evidence change refits (RouteIQ-f9e9). \"\n \"Default off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "6e1e2abd684ea3a19d4bbc9116c3775db0ce3a4b": {"t": 1785769099.3432405, "n": 58220, "h": "0640df480c5a3a56", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n banned_models: list[str] = Field(\n default_factory=list,\n description=(\n \"Models removed from the routable candidate set BEFORE the routing \"\n \"strategy/bandit scores (data-retention / compliance ban, e.g. \"\n \"Fable 5). Matched against ``litellm_params.model`` (the bandit arm \"\n \"key) and ``model_name`` (the group name). Enforced RouteIQ-native \"\n \"(see ``candidate_filter.py``), NOT via LiteLLM's \"\n \"``async_filter_deployments`` hook -- that hook lives on the \"\n \"built-in selection path the custom strategy bypasses. Default empty \"\n \"(no ban) -> byte-stable no-op. \"\n \"Env: ``ROUTEIQ_GOVERNANCE__BANNED_MODELS``.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps;\n Redis (hot) / Aurora (durable) backends are wired behind ``backend`` /\n ``durable`` flags for the live substrate (P1/P2), default off.\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=\"Hot posterior backend: memory | redis.\",\n )\n durable: str = Field(\n \"none\",\n description=\"Durable posterior store: none | aurora.\",\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Mixing rate of the cost term into the reward signal.\",\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n moment_fit: bool = Field(\n False,\n description=(\n \"Use the doc-20 §3.1 option-2 cached Newton moment-fit \"\n \"Beta(alpha,beta) -> Kumaraswamy(a,b) mapping (matches mean + \"\n \"variance) instead of the option-1 ``a=alpha, b=beta`` shortcut. \"\n \"The shortcut distorts the posterior mean (Beta(51,51) mean 0.5 -> \"\n \"Kumaraswamy 0.9155) and can invert the exploit decision; the \"\n \"moment-fit restores the correct mean. Cached per-posterior and \"\n \"refreshed only when ``alpha+beta`` crosses a log-spaced threshold, \"\n \"keeping the hot path at ~1 uniform draw + the quantile. Default \"\n \"off for byte-stable backward-compat. \"\n \"Env: ``ROUTEIQ_KUMARASWAMY_THOMPSON__MOMENT_FIT``.\"\n ),\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "80ef50e4bb6b7e40da5d54944ec6071f7bea4dd8": {"t": 1785769100.7435608, "n": 56645, "h": "c8d766633f2a533b", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass GovernanceFailMode(str, Enum):\n \"\"\"Governance budget/rate enforcement behaviour when the spend store fails.\n\n ``OPEN`` (default, back-compat) allows the request when the store\n (ElastiCache/Aurora) cannot confirm spend or RPM -- the historical\n fail-open behaviour. ``CLOSED`` denies the request when a limit IS\n configured but the store is unavailable, so a store outage cannot leak\n spend/rate past the configured budget/RPM (RouteIQ-24fc).\n \"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass GovernanceSettings(BaseModel):\n \"\"\"Workspace/key governance budget + rate-limit enforcement behaviour.\n\n Env vars: nested ``ROUTEIQ_GOVERNANCE__FAIL_MODE`` (the ``__`` nested\n delimiter on :class:`GatewaySettings`). The consumer in ``governance.py``\n ALSO honours the legacy flat ``ROUTEIQ_GOVERNANCE_FAIL_MODE`` via an\n ``os.getenv`` fallback (mirrors the ``QuotaConfig.from_env`` pattern), so\n operators can use either form.\n \"\"\"\n\n fail_mode: GovernanceFailMode = Field(\n GovernanceFailMode.OPEN,\n description=(\n \"Behaviour when the governance spend store (ElastiCache/Aurora) is \"\n \"unavailable. OPEN (default) allows; CLOSED denies when a budget/RPM \"\n \"limit is configured but the store cannot confirm current usage.\"\n ),\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps;\n Redis (hot) / Aurora (durable) backends are wired behind ``backend`` /\n ``durable`` flags for the live substrate (P1/P2), default off.\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=\"Hot posterior backend: memory | redis.\",\n )\n durable: str = Field(\n \"none\",\n description=\"Durable posterior store: none | aurora.\",\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Mixing rate of the cost term into the reward signal.\",\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n governance: GovernanceSettings = Field(\n default_factory=GovernanceSettings, # type: ignore[arg-type]\n description=\"Workspace/key governance budget + rate-limit settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "08341e75bf658ace3e430b4854550c1e9ac5cab9": {"t": 1785769102.412936, "n": 55107, "h": "128151d231825ee5", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps;\n Redis (hot) / Aurora (durable) backends are wired behind ``backend`` /\n ``durable`` flags for the live substrate (P1/P2), default off.\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=\"Hot posterior backend: memory | redis.\",\n )\n durable: str = Field(\n \"none\",\n description=\"Durable posterior store: none | aurora.\",\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Mixing rate of the cost term into the reward signal.\",\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n llmrouter_governance_spend_tracking: bool = Field(\n True,\n description=(\n \"Write post-response governance spend/RPM counters + usage-policy \"\n \"cost/token counters (P4 spend write path; env \"\n \"LLMROUTER_GOVERNANCE_SPEND_TRACKING). Independent of OTEL/telemetry.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "51d42eed7a432695b315d45f56277e754fc76349": {"t": 1785769104.1878338, "n": 54774, "h": "d4e826a4dcbed04d", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n error_log_enabled: bool = Field(\n True,\n description=(\n \"Emit a structured error JSON line (top-level lowercased \"\n '``\"level\": \"error\"`` + ``\"event\": \"error\"``) on the dedicated '\n \"routing logger from the error path (ADR-0027 P2 hardening, \"\n \"RouteIQ-731c). This is the field the CloudWatch ``RouterErrorFilter`` \"\n 'selects on (``$.level = \"error\"``); without it the router-error-count '\n \"alarm can never fire because no emitter produces a top-level ``level`` \"\n \"key. PII-safe: carries the error type + a scrubbed error message only. \"\n \"Disable to suppress the line \"\n \"(env: ROUTEIQ_OTEL__ERROR_LOG_ENABLED).\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n # --- AWS AppConfig runtime retrieval (ADR-0026, RouteIQ-4333) -------------\n # Poll a deployed AppConfig configuration profile via the AppConfigData\n # data-plane API. Off by default so the chart/app stays cloud-agnostic. The\n # pod-role grant for this is RouteIQ-569f (appconfigdata:StartConfigurationSession\n # + appconfigdata:GetLatestConfiguration scoped to the profile ARN). Env vars\n # are nested under the ``config_sync`` prefix\n # (e.g. ROUTEIQ_CONFIG_SYNC__APPCONFIG_ENABLED).\n appconfig_enabled: bool = Field(\n False,\n description=(\n \"Enable runtime AWS AppConfig retrieval (ADR-0026). Polls a deployed \"\n \"AppConfig configuration profile via the AppConfigData data-plane API \"\n \"(StartConfigurationSession + GetLatestConfiguration) and writes a \"\n \"changed body to the local config path. Default off.\"\n ),\n )\n appconfig_application: Optional[str] = Field(\n None,\n description=(\n \"AppConfig application id or name to poll (CfnOutput \"\n \"AppConfigApplicationId). Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_environment: Optional[str] = Field(\n None,\n description=(\n \"AppConfig environment id or name to poll (e.g. the deploy env). \"\n \"Required when appconfig_enabled is true.\"\n ),\n )\n appconfig_profile: Optional[str] = Field(\n None,\n description=(\n \"AppConfig configuration profile id or name to poll. Required when \"\n \"appconfig_enabled is true.\"\n ),\n )\n appconfig_poll_interval_seconds: int = Field(\n 60,\n ge=15,\n description=(\n \"Requested AppConfig poll interval in seconds (the data-plane API \"\n \"enforces a minimum of 15s; the loop honors the server's \"\n \"NextPollIntervalInSeconds when shorter).\"\n ),\n )\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps;\n Redis (hot) / Aurora (durable) backends are wired behind ``backend`` /\n ``durable`` flags for the live substrate (P1/P2), default off.\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=\"Hot posterior backend: memory | redis.\",\n )\n durable: str = Field(\n \"none\",\n description=\"Durable posterior store: none | aurora.\",\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Mixing rate of the cost term into the reward signal.\",\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "506fb897bdf732a587f3fdd1d1d6e0f4d8d1ea57": {"t": 1785769105.8255725, "n": 52077, "h": "7384d8866792f4da", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass KumaraswamyThompsonSettings(BaseModel):\n \"\"\"Kumaraswamy-Thompson online-bandit routing strategy configuration.\n\n The core bandit runs with the in-memory backend and NO external deps;\n Redis (hot) / Aurora (durable) backends are wired behind ``backend`` /\n ``durable`` flags for the live substrate (P1/P2), default off.\n\n Env vars: ``ROUTEIQ_KUMARASWAMY_THOMPSON__ENABLED``,\n ``ROUTEIQ_KUMARASWAMY_THOMPSON__BACKEND``, etc. (``__`` nested delimiter).\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Register the Kumaraswamy-Thompson strategy at startup.\",\n )\n backend: str = Field(\n \"memory\",\n description=\"Hot posterior backend: memory | redis.\",\n )\n durable: str = Field(\n \"none\",\n description=\"Durable posterior store: none | aurora.\",\n )\n w_quality: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on response quality.\",\n )\n w_cost: float = Field(\n 0.4,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on cost (cheaper => higher).\",\n )\n w_latency: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Reward weight on latency (faster => higher).\",\n )\n cost_reward_alpha: float = Field(\n 0.5,\n ge=0.0,\n le=1.0,\n description=\"Mixing rate of the cost term into the reward signal.\",\n )\n decay_gamma: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay toward the prior (re-opens exploration).\",\n )\n cold_start_kappa: float = Field(\n 5.0,\n ge=0.0,\n le=50.0,\n description=\"Warm-start pseudo-count from the static quality table.\",\n )\n seed: Optional[int] = Field(\n None,\n description=\"RNG seed for deterministic sampling in tests/replay.\",\n )\n\n\nclass AdapterFrameworkSettings(BaseModel):\n \"\"\"Strategy-agnostic routing-adapter framework configuration.\n\n Env vars: ``ROUTEIQ_ADAPTER_FRAMEWORK__ENTRYPOINT_DISCOVERY``, etc.\n \"\"\"\n\n entrypoint_discovery: bool = Field(\n False,\n description=\"Discover out-of-tree adapters via entry-points at startup.\",\n )\n capability_negotiation: bool = Field(\n False,\n description=\"Enable the pre-selection required-signals filter.\",\n )\n mlops_feedback_loop: bool = Field(\n False,\n description=(\n \"Wire the eval-pipeline FEEDBACK arm into the strategy-agnostic \"\n \"MLOps coordinator: discover continuous-train learning adapters \"\n \"from the registry and subscribe the coordinator to \"\n \"EvalPipeline.feedback_callbacks. Default off.\"\n ),\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n kumaraswamy_thompson: KumaraswamyThompsonSettings = Field(\n default_factory=KumaraswamyThompsonSettings, # type: ignore[arg-type]\n description=\"Kumaraswamy-Thompson bandit routing strategy settings.\",\n )\n adapter_framework: AdapterFrameworkSettings = Field(\n default_factory=AdapterFrameworkSettings, # type: ignore[arg-type]\n description=\"Strategy-agnostic routing-adapter framework settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "7fda2923c5b0e2cf372ed039d691e4997907fc64": {"t": 1785769107.3398726, "n": 48899, "h": "929b3749a85fa939", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n username: Optional[str] = Field(\n None,\n description=(\n \"Redis ACL/IAM username (REDIS_USERNAME). On the ADR-0029 IAM-auth path \"\n \"this is the CacheIamUserName (user_id == user_name). Read directly from \"\n \"the legacy REDIS_USERNAME env var by redis_pool._redis_settings().\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint an elasticache:Connect SigV4 token as the AUTH and present \"\n \"username as the IAM user (ADR-0029) instead of a static REDIS_PASSWORD. \"\n \"Env: ROUTEIQ_REDIS_IAM_AUTH (flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the elasticache:Connect signer. Falls back to host \"\n \"parse / AWS_REGION. Serverless endpoints use shortened region tokens \"\n \"(use1), so set this or AWS_REGION explicitly on the serverless path.\"\n ),\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n iam_auth: bool = Field(\n False,\n description=(\n \"Mint a short-lived (15-min) rds-db:connect IAM token per connection \"\n \"instead of using a static password (ADR-0028). Env: ROUTEIQ_DB_IAM_AUTH \"\n \"(flat) -- mapped onto this nested field by \"\n \"GatewaySettings._map_flat_iam_aliases. Default OFF (static-cred path \"\n \"unchanged).\"\n ),\n )\n iam_region: Optional[str] = Field(\n None,\n description=(\n \"AWS region for the rds-db:connect signer. Falls back to host parse \"\n \"(.<region>.rds.amazonaws.com) / AWS_REGION.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @model_validator(mode=\"before\")\n @classmethod\n def _map_flat_iam_aliases(cls, data: Any) -> Any:\n \"\"\"Map the flat IAM-auth env vars onto the nested settings fields.\n\n The chart/README emit the FLAT env names ``ROUTEIQ_DB_IAM_AUTH`` and\n ``ROUTEIQ_REDIS_IAM_AUTH`` (ADR-0028 / ADR-0029). pydantic-settings\n does NOT resolve a ``validation_alias`` on a nested ``BaseModel`` field\n from a flat top-level env var (only the nested ``ROUTEIQ_POSTGRES__*`` /\n ``ROUTEIQ_REDIS__*`` form would work, which the chart does not emit), so\n we inject the flat values onto the nested ``postgres`` / ``redis`` dicts\n here, before validation. The flat env var is the source of truth the\n chart already targets. Both flags DEFAULT OFF (only set when the flat\n env var is present and truthy), so the static-cred path is unchanged.\n \"\"\"\n import os as _os\n\n def _truthy(value: Any) -> bool:\n return str(value).strip().lower() in (\"true\", \"1\", \"yes\", \"on\")\n\n if not isinstance(data, dict):\n return data\n\n flat_map = (\n (\"ROUTEIQ_DB_IAM_AUTH\", \"postgres\"),\n (\"ROUTEIQ_REDIS_IAM_AUTH\", \"redis\"),\n )\n for env_name, section in flat_map:\n raw = _os.environ.get(env_name)\n if raw is None or not _truthy(raw):\n continue\n current = data.get(section)\n if current is None:\n data[section] = {\"iam_auth\": True}\n elif isinstance(current, dict):\n # Do not clobber an explicit nested override if one was given.\n current.setdefault(\"iam_auth\", True)\n elif isinstance(current, BaseModel):\n # An already-constructed nested model (e.g. init kwargs); set\n # the flag only when it was left at its default.\n if getattr(current, \"iam_auth\", False) is False:\n data[section] = current.model_copy(update={\"iam_auth\": True})\n return data\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "496c242f6e0dc148877d08102f33e0ff8ffb1c27": {"t": 1785769108.901696, "n": 45056, "h": "88eb16e6e35f3a31", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n routing_decision_log_enabled: bool = Field(\n True,\n description=(\n \"Emit the structured ``routing_decision`` JSON log line per request \"\n \"(ADR-0027 P2 data-lake / CloudWatch metric-filter source). PII-safe: \"\n \"carries model names, token counts, latency, and a query_length only - \"\n \"never prompt or completion text. Disable to suppress the line \"\n \"entirely (env: ROUTEIQ_OTEL__ROUTING_DECISION_LOG_ENABLED).\"\n ),\n )\n routing_decision_logger_name: str = Field(\n \"routeiq.routing_decision\",\n description=(\n \"Dedicated logger name the structured routing_decision JSON line is \"\n \"emitted on. Routed to the dedicated CloudWatch routing log group by \"\n \"the Fluent Bit pipeline (the data-lake + dimensioned metric filter \"\n \"source). Kept off the root logger so it is independently routable.\"\n ),\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "6123c6abe672ca227ca8c12781f74692a442e0a5": {"t": 1785769110.3390033, "n": 44103, "h": "1adbb2260f930b00", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings, # type: ignore[arg-type]\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings, # type: ignore[arg-type]\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings, # type: ignore[arg-type]\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings, # type: ignore[arg-type]\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings, # type: ignore[arg-type]\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings, # type: ignore[arg-type]\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings, # type: ignore[arg-type]\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings, # type: ignore[arg-type]\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings, # type: ignore[arg-type]\n description=\"Quota enforcement settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings, # type: ignore[arg-type]\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings, # type: ignore[arg-type]\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings, # type: ignore[arg-type]\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings, # type: ignore[arg-type]\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings, # type: ignore[arg-type]\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings, # type: ignore[arg-type]\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings, # type: ignore[arg-type]\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings, # type: ignore[arg-type]\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings, # type: ignore[arg-type]\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings, # type: ignore[arg-type]\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings, # type: ignore[arg-type]\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings, # type: ignore[arg-type]\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings, # type: ignore[arg-type]\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings, # type: ignore[arg-type]\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings, # type: ignore[arg-type]\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings, # type: ignore[arg-type]\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings, # type: ignore[arg-type]\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings, # type: ignore[arg-type]\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings, # type: ignore[arg-type]\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings, # type: ignore[arg-type]\n description=\"Skills discovery plugin settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}, "ba89b9ec7dac0615f7aa7e0ba75854d1d147d28b": {"t": 1785769111.9262831, "n": 43349, "h": "8fe31e0e4f920dc7", "c": "\"\"\"Typed configuration for RouteIQ Gateway.\n\nReplaces 124+ scattered ``os.environ.get()`` calls with a single validated\nsettings object. All fields are documented, typed, and validated at startup.\n\nThe settings module is **purely additive** — existing code continues to\nread ``os.environ`` as before. Modules can migrate to ``get_settings()``\nincrementally without a flag day.\n\nUsage::\n\n from litellm_llmrouter.settings import get_settings\n\n settings = get_settings()\n if settings.redis.host:\n ...\n\nSingleton & Reset Pattern:\n Follows the codebase convention of ``get_*()`` / ``reset_*()`` pairs.\n Tests **must** call ``reset_settings()`` in an ``autouse=True`` fixture.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field, field_validator, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nlogger = logging.getLogger(__name__)\n\n# ============================================================================\n# Enums\n# ============================================================================\n\n\nclass RoutingProfile(str, Enum):\n \"\"\"Routing profiles for centroid-based routing.\n\n Profiles control the cost/quality trade-off for model selection:\n - ``auto`` — Let the classifier decide based on prompt complexity.\n - ``eco`` — Prefer cheaper models when quality is sufficient.\n - ``premium`` — Prefer the highest-quality models regardless of cost.\n - ``free`` — Only use free-tier models.\n - ``reasoning`` — Prefer models with strong reasoning capabilities.\n \"\"\"\n\n AUTO = \"auto\"\n ECO = \"eco\"\n PREMIUM = \"premium\"\n FREE = \"free\"\n REASONING = \"reasoning\"\n\n\nclass PolicyFailMode(str, Enum):\n \"\"\"Policy engine behaviour when evaluation fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass AuditFailMode(str, Enum):\n \"\"\"Audit logging behaviour when the audit backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\nclass QuotaFailMode(str, Enum):\n \"\"\"Quota enforcement behaviour when the quota backend fails.\"\"\"\n\n OPEN = \"open\"\n CLOSED = \"closed\"\n\n\n# ============================================================================\n# Nested Settings Models\n# ============================================================================\n\n\nclass RedisSettings(BaseModel):\n \"\"\"Redis configuration. Optional — gateway works without Redis.\n\n Used by: caching, HA leader election, quota enforcement, MCP HA sync,\n conversation affinity, and health checks.\n\n Env vars: ``REDIS_HOST``, ``REDIS_PORT``, ``REDIS_PASSWORD``,\n ``REDIS_SSL``, ``REDIS_DB``.\n \"\"\"\n\n host: Optional[str] = Field(\n None,\n description=\"Redis host. When unset, Redis features are disabled.\",\n )\n port: int = Field(\n 6379,\n ge=1,\n le=65535,\n description=\"Redis port.\",\n )\n password: Optional[str] = Field(\n None,\n description=\"Redis password. Omit for unauthenticated connections.\",\n )\n ssl: bool = Field(\n False,\n description=\"Enable TLS when connecting to Redis.\",\n )\n db: int = Field(\n 0,\n ge=0,\n le=15,\n description=\"Redis database index.\",\n )\n\n\nclass PostgresSettings(BaseModel):\n \"\"\"PostgreSQL configuration. Optional — gateway works without a DB.\n\n Env var: ``DATABASE_URL``.\n \"\"\"\n\n url: Optional[str] = Field(\n None,\n description=(\n \"PostgreSQL connection URL (e.g. ``postgresql://user:pass@host:5432/db``).\"\n ),\n )\n pool_min: int = Field(\n 2,\n ge=1,\n le=50,\n description=\"Minimum connection pool size.\",\n )\n pool_max: int = Field(\n 10,\n ge=1,\n le=100,\n description=\"Maximum connection pool size.\",\n )\n ssl_mode: str = Field(\n \"prefer\",\n description=(\n \"SSL mode: disable, allow, prefer, require, verify-ca, verify-full.\"\n ),\n )\n\n @field_validator(\"url\")\n @classmethod\n def _validate_url_scheme(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n valid = (\"postgresql://\", \"postgres://\", \"sqlite://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"DATABASE_URL does not start with a recognised scheme \"\n f\"(expected one of {', '.join(valid)})\",\n stacklevel=2,\n )\n return v\n\n\nclass OTelSettings(BaseModel):\n \"\"\"OpenTelemetry observability configuration.\n\n Env vars: ``OTEL_ENABLED``, ``OTEL_EXPORTER_OTLP_ENDPOINT``,\n ``OTEL_SERVICE_NAME``, ``OTEL_SAMPLE_RATE``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable OpenTelemetry traces, metrics, and logs.\",\n )\n endpoint: Optional[str] = Field(\n None,\n description=(\n \"OTLP collector endpoint \"\n \"(e.g. ``http://localhost:4317``). Supports http, https, grpc.\"\n ),\n )\n service_name: str = Field(\n \"litellm-gateway\",\n description=\"OTel service.name resource attribute.\",\n )\n sample_rate: float = Field(\n 1.0,\n ge=0.0,\n le=1.0,\n description=\"Trace sampling rate (0.0 = none, 1.0 = all).\",\n )\n resource_service_name: str = Field(\n \"routeiq\",\n description=\"OTel resource attribute service.name for RouteIQ metrics.\",\n )\n deployment_env: str = Field(\n \"default\",\n description=\"OTel resource attribute deployment.environment.\",\n )\n metrics_namespace: str = Field(\n \"RouteIQ\",\n description=\"Namespace prefix for RouteIQ metrics.\",\n )\n\n @field_validator(\"endpoint\")\n @classmethod\n def _validate_endpoint(cls, v: Optional[str]) -> Optional[str]:\n if v is not None:\n valid = (\"http://\", \"https://\", \"grpc://\")\n if not v.startswith(valid):\n warnings.warn(\n f\"OTEL_EXPORTER_OTLP_ENDPOINT ('{v}') does not start \"\n f\"with a recognised scheme (expected http, https, or grpc)\",\n stacklevel=2,\n )\n return v\n\n\nclass OIDCSettings(BaseModel):\n \"\"\"OIDC / SSO integration configuration.\n\n Env vars: ``ROUTEIQ_OIDC_*``. Requires the ``authlib`` package\n (install via ``pip install routeiq[oidc]``).\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable OIDC authentication.\")\n issuer_url: Optional[str] = Field(\n None,\n description=\"OIDC issuer URL (must support .well-known/openid-configuration).\",\n )\n client_id: Optional[str] = Field(None, description=\"OAuth2 client ID.\")\n client_secret: Optional[str] = Field(None, description=\"OAuth2 client secret.\")\n user_id_claim: str = Field(\n \"sub\", description=\"JWT claim for unique user identifier.\"\n )\n email_claim: str = Field(\"email\", description=\"JWT claim for email.\")\n display_name_claim: str = Field(\"name\", description=\"JWT claim for display name.\")\n team_claim: Optional[str] = Field(\n None, description=\"JWT claim for team assignment.\"\n )\n org_claim: Optional[str] = Field(None, description=\"JWT claim for organization.\")\n role_claim: Optional[str] = Field(None, description=\"JWT claim for roles.\")\n role_mapping: Dict[str, List[str]] = Field(\n default_factory=dict,\n description=\"Maps IdP role names to lists of RouteIQ internal roles (JSON).\",\n )\n default_role: str = Field(\n \"internal_user\",\n description=\"Default role when no mapping matches.\",\n )\n allowed_email_domains: List[str] = Field(\n default_factory=list,\n description=\"Restrict logins to these email domains (empty = all allowed).\",\n )\n auto_provision_users: bool = Field(\n True, description=\"Auto-create user records on first SSO login.\"\n )\n session_ttl: int = Field(\n 1800,\n ge=60,\n le=86400,\n description=\"Validated identity cache TTL in seconds.\",\n )\n token_exchange_enabled: bool = Field(\n True,\n description=\"Enable the /auth/token-exchange endpoint.\",\n )\n max_key_ttl_days: int = Field(\n 365,\n ge=1,\n le=3650,\n description=\"Maximum allowed TTL for exchanged API keys.\",\n )\n default_key_ttl_days: int = Field(\n 90,\n ge=1,\n le=3650,\n description=\"Default API key TTL when not specified.\",\n )\n\n @field_validator(\"issuer_url\")\n @classmethod\n def _validate_issuer_url(cls, v: Optional[str]) -> Optional[str]:\n if v is None:\n return v\n v = v.rstrip(\"/\")\n if not v.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n raise ValueError(\n \"OIDC issuer_url must use HTTPS (HTTP allowed only for localhost)\"\n )\n return v\n\n\nclass RoutingSettings(BaseModel):\n \"\"\"ML routing strategy configuration.\n\n Env vars: ``ROUTEIQ_USE_PLUGIN_STRATEGY``, ``ROUTEIQ_CENTROID_ROUTING``,\n ``ROUTEIQ_CENTROID_WARMUP``, ``ROUTEIQ_ROUTING_PROFILE``,\n ``ROUTEIQ_ROUTING_STRATEGY``, ``LLMROUTER_USE_PIPELINE``,\n ``LLMROUTER_ACTIVE_ROUTING_STRATEGY``, ``LLMROUTER_STRATEGY_WEIGHTS``,\n ``LLMROUTER_EXPERIMENT_ID``, ``LLMROUTER_EXPERIMENT_CONFIG``.\n \"\"\"\n\n # use_plugin_strategy is always True. The legacy monkey-patch mode has\n # been removed. This property exists for backward compatibility with code\n # that reads ``settings.routing.use_plugin_strategy``.\n use_plugin_strategy: bool = Field(\n True,\n description=\"Always True. Legacy monkey-patch mode has been removed.\",\n )\n centroid_enabled: bool = Field(\n True,\n description=\"Enable centroid-based zero-config routing fallback.\",\n )\n centroid_warmup: bool = Field(\n False,\n description=\"Pre-warm centroid classifier at startup.\",\n )\n default_profile: RoutingProfile = Field(\n RoutingProfile.AUTO,\n description=\"Default routing profile (auto/eco/premium/free/reasoning).\",\n )\n strategy_override: Optional[str] = Field(\n None,\n description=(\n \"Explicit routing strategy override \"\n \"(e.g. ``llmrouter-knn``). Takes priority over config.\"\n ),\n )\n pipeline_enabled: bool = Field(\n True,\n description=\"Enable A/B pipeline routing via strategy registry.\",\n )\n active_strategy: Optional[str] = Field(\n None,\n description=\"Active routing strategy name for A/B routing.\",\n )\n strategy_weights: Optional[str] = Field(\n None,\n description=\"JSON dict of strategy name → weight for A/B routing.\",\n )\n experiment_id: Optional[str] = Field(\n None,\n description=\"Experiment identifier for A/B routing telemetry.\",\n )\n experiment_config: Optional[str] = Field(\n None,\n description=\"JSON config for A/B experiment (parsed at runtime).\",\n )\n personalized_enabled: bool = Field(\n False,\n description=(\n \"Enable personalized routing. Learns per-user/per-team model \"\n \"preferences from feedback signals and adapts routing decisions.\"\n ),\n )\n personalized_dim: int = Field(\n 128,\n ge=16,\n le=1024,\n description=\"Dimensionality of user preference vectors.\",\n )\n personalized_learning_rate: float = Field(\n 0.1,\n ge=0.001,\n le=1.0,\n description=\"Learning rate for preference updates.\",\n )\n personalized_decay: float = Field(\n 0.99,\n ge=0.5,\n le=1.0,\n description=\"Per-day decay factor for stale preferences (1.0 = no decay).\",\n )\n\n\nclass SecuritySettings(BaseModel):\n \"\"\"Security, auth, and access control configuration.\n\n Env vars: ``ADMIN_API_KEYS``, ``ADMIN_API_KEY``, ``ADMIN_AUTH_ENABLED``,\n ``ROUTEIQ_KEY_PREFIX``, ``ROUTEIQ_CORS_ORIGINS``,\n ``ROUTEIQ_CORS_CREDENTIALS``, ``LLMROUTER_ENFORCE_SIGNED_MODELS``,\n ``LLMROUTER_ALLOW_PICKLE_MODELS``, ``LLMROUTER_STRICT_PICKLE_MODE``,\n ``LLMROUTER_PICKLE_ALLOWLIST``.\n \"\"\"\n\n admin_api_keys: Optional[str] = Field(\n None,\n description=\"Comma-separated admin API keys for control-plane auth.\",\n )\n admin_api_key: Optional[str] = Field(\n None,\n description=\"Single admin API key (legacy, prefer ADMIN_API_KEYS).\",\n )\n admin_auth_enabled: bool = Field(\n True,\n description=(\n \"Enable admin auth. Setting to false is NOT recommended for production.\"\n ),\n )\n key_prefix: str = Field(\n \"sk-riq-\",\n description=\"Prefix applied to master/admin keys.\",\n )\n cors_origins: str = Field(\n \"\",\n description=\"Comma-separated allowed CORS origins. Empty = no CORS (must be explicitly configured).\",\n )\n cors_credentials: bool = Field(\n False,\n description=\"Allow credentials in CORS requests.\",\n )\n enforce_signed_models: bool = Field(\n True,\n description=\"Require cryptographic verification of ML model artifacts.\",\n )\n allow_pickle_models: bool = Field(\n False,\n description=(\n \"Allow pickle model loading. Security risk — only enable \"\n \"in trusted environments.\"\n ),\n )\n strict_pickle_mode: bool = Field(\n False,\n description=\"Strict pickle mode (hash-only allowlist).\",\n )\n pickle_allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated SHA-256 hashes of allowed pickle files.\",\n )\n\n @model_validator(mode=\"after\")\n def _warn_pickle_without_signing(self) -> \"SecuritySettings\":\n if self.allow_pickle_models and not self.enforce_signed_models:\n warnings.warn(\n \"LLMROUTER_ALLOW_PICKLE_MODELS is enabled but \"\n \"LLMROUTER_ENFORCE_SIGNED_MODELS is not. This allows \"\n \"loading unverified pickle models (code-execution risk).\",\n stacklevel=2,\n )\n return self\n\n\nclass PolicySettings(BaseModel):\n \"\"\"OPA-style policy engine configuration.\n\n Env vars: ``POLICY_ENGINE_ENABLED``, ``POLICY_ENGINE_FAIL_MODE``,\n ``POLICY_CONFIG_PATH``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the policy engine.\")\n fail_mode: PolicyFailMode = Field(\n PolicyFailMode.OPEN,\n description=\"Behaviour on policy evaluation errors (open/closed).\",\n )\n config_path: Optional[str] = Field(\n None,\n description=\"Path to the policy YAML config file.\",\n )\n\n\nclass AuditSettings(BaseModel):\n \"\"\"Audit logging configuration.\n\n Env vars: ``AUDIT_LOG_ENABLED``, ``AUDIT_LOG_FAIL_MODE``.\n \"\"\"\n\n enabled: bool = Field(True, description=\"Enable audit logging.\")\n fail_mode: AuditFailMode = Field(\n AuditFailMode.OPEN,\n description=\"Behaviour when the audit backend fails.\",\n )\n\n\nclass QuotaSettings(BaseModel):\n \"\"\"Per-team / per-key quota enforcement.\n\n Env vars: ``ROUTEIQ_QUOTA_ENABLED``, ``ROUTEIQ_QUOTA_FAIL_MODE``,\n ``ROUTEIQ_QUOTA_LIMITS_JSON``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable quota enforcement.\")\n fail_mode: QuotaFailMode = Field(\n QuotaFailMode.OPEN,\n description=\"Behaviour when quota backend is unavailable.\",\n )\n limits_json: Optional[str] = Field(\n None,\n description=\"JSON-encoded quota limits per team/key.\",\n )\n default_spend_per_1k_tokens: float = Field(\n 0.002,\n ge=0.0,\n description=\"Default USD cost per 1K tokens when LiteLLM cost is unavailable.\",\n )\n cost_reconciliation_enabled: bool = Field(\n True,\n description=\"Enable post-response cost reconciliation.\",\n )\n\n\nclass MCPSettings(BaseModel):\n \"\"\"MCP (Model Context Protocol) gateway configuration.\n\n Env vars: ``MCP_GATEWAY_ENABLED``, ``LLMROUTER_ENABLE_MCP_TOOL_INVOCATION``,\n ``MCP_HA_SYNC_ENABLED``, ``MCP_SYNC_INTERVAL``, ``MCP_OAUTH_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the MCP gateway.\")\n tool_invocation_enabled: bool = Field(\n False,\n description=(\n \"Enable remote tool invocation. Disabled by default for security.\"\n ),\n )\n ha_sync_enabled: bool = Field(\n True,\n description=\"Enable HA sync of MCP server registry via Redis.\",\n )\n sync_interval: float = Field(\n 5.0,\n ge=1.0,\n description=\"Interval (seconds) for HA registry sync.\",\n )\n oauth_enabled: bool = Field(False, description=\"Enable OAuth for MCP.\")\n\n\nclass A2ASettings(BaseModel):\n \"\"\"A2A (Agent-to-Agent) gateway configuration.\n\n Env vars: ``A2A_GATEWAY_ENABLED``, ``A2A_BASE_URL``,\n ``A2A_RAW_STREAMING_ENABLED``, ``A2A_RAW_STREAMING_CHUNK_SIZE``,\n ``A2A_TASK_TTL_SECONDS``, ``A2A_TASK_STORE_MAX_TASKS``,\n ``A2A_TASK_RATE_LIMIT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the A2A gateway convenience routes.\",\n )\n base_url: str = Field(\n \"\",\n description=\"Base URL for A2A agent communications.\",\n )\n raw_streaming_enabled: bool = Field(\n False,\n description=\"Use raw byte streaming (aiter_bytes) for true passthrough.\",\n )\n raw_streaming_chunk_size: int = Field(\n 8192,\n ge=1024,\n description=\"Chunk size in bytes for raw streaming mode.\",\n )\n task_ttl_seconds: int = Field(\n 3600,\n ge=60,\n description=\"TTL in seconds for A2A tasks in memory.\",\n )\n task_store_max_tasks: int = Field(\n 10000,\n ge=100,\n description=\"Maximum tasks in the A2A task store.\",\n )\n task_rate_limit: int = Field(\n 100,\n ge=1,\n description=\"Per-agent rate limit: max task creates per minute.\",\n )\n\n\nclass ResilienceSettings(BaseModel):\n \"\"\"Backpressure, drain, and circuit-breaker configuration.\n\n Env vars: ``ROUTEIQ_MAX_CONCURRENT_REQUESTS``,\n ``ROUTEIQ_DRAIN_TIMEOUT_SECONDS``, ``ROUTEIQ_BACKPRESSURE_EXCLUDED_PATHS``,\n ``ROUTEIQ_PROVIDER_CB_ENABLED``.\n \"\"\"\n\n max_concurrent_requests: int = Field(\n 0,\n ge=0,\n description=\"Max concurrent requests (0 = unlimited).\",\n )\n drain_timeout_seconds: float = Field(\n 30.0,\n ge=0.0,\n description=\"Graceful shutdown drain timeout in seconds.\",\n )\n backpressure_excluded_paths: str = Field(\n \"\",\n description=\"Comma-separated paths excluded from backpressure.\",\n )\n provider_circuit_breaker_enabled: bool = Field(\n False,\n description=\"Enable per-provider circuit breakers.\",\n )\n\n\nclass ConfigSyncSettings(BaseModel):\n \"\"\"Remote config sync from S3/GCS.\n\n Env vars: ``CONFIG_HOT_RELOAD``, ``CONFIG_SYNC_ENABLED``,\n ``CONFIG_S3_BUCKET``, ``CONFIG_S3_KEY``, ``CONFIG_GCS_BUCKET``,\n ``CONFIG_GCS_KEY``.\n \"\"\"\n\n hot_reload: bool = Field(\n False,\n description=\"Enable filesystem-watching config hot-reload.\",\n )\n sync_enabled: bool = Field(\n True,\n description=\"Enable background config sync.\",\n )\n s3_bucket: Optional[str] = Field(\n None, description=\"S3 bucket for remote config sync.\"\n )\n s3_key: Optional[str] = Field(None, description=\"S3 key for the config file.\")\n gcs_bucket: Optional[str] = Field(\n None, description=\"GCS bucket for remote config sync.\"\n )\n gcs_key: Optional[str] = Field(None, description=\"GCS key for the config file.\")\n\n\nclass HTTPClientSettings(BaseModel):\n \"\"\"Shared HTTP client pool configuration.\n\n Env vars: ``ROUTEIQ_HTTP_CLIENT_POOLING_ENABLED``,\n ``HTTP_CLIENT_MAX_CONNECTIONS``, ``HTTP_CLIENT_MAX_KEEPALIVE``,\n ``HTTP_CLIENT_DEFAULT_TIMEOUT``.\n \"\"\"\n\n pooling_enabled: bool = Field(\n True,\n description=\"Enable shared HTTP client connection pooling.\",\n )\n max_connections: int = Field(\n 100,\n ge=1,\n description=\"Maximum total connections in the pool.\",\n )\n max_keepalive: int = Field(\n 20,\n ge=1,\n description=\"Maximum keepalive connections.\",\n )\n default_timeout: float = Field(\n 60.0,\n ge=1.0,\n description=\"Default request timeout in seconds.\",\n )\n\n\nclass SSRFSettings(BaseModel):\n \"\"\"SSRF protection configuration.\n\n Env vars: ``LLMROUTER_OUTBOUND_URL_ALLOWLIST``,\n ``LLMROUTER_SSRF_USE_SYNC_DNS``, ``LLMROUTER_SSRF_DNS_TIMEOUT``,\n ``LLMROUTER_SSRF_DNS_CACHE_TTL``, ``LLMROUTER_SSRF_DNS_CACHE_SIZE``.\n \"\"\"\n\n outbound_url_allowlist: str = Field(\n \"\",\n description=\"Comma-separated URLs to allow through SSRF checks.\",\n )\n use_sync_dns: bool = Field(\n False,\n description=\"Use synchronous DNS resolution in SSRF checks.\",\n )\n dns_timeout: float = Field(\n 2.0,\n ge=0.1,\n description=\"DNS resolution timeout in seconds.\",\n )\n dns_cache_ttl: int = Field(\n 300,\n ge=0,\n description=\"DNS cache TTL in seconds.\",\n )\n dns_cache_size: int = Field(\n 256,\n ge=1,\n description=\"DNS cache maximum entries.\",\n )\n\n\nclass ConversationAffinitySettings(BaseModel):\n \"\"\"Conversation-based routing affinity.\n\n Env vars: ``CONVERSATION_AFFINITY_ENABLED``,\n ``CONVERSATION_AFFINITY_TTL``, ``CONVERSATION_AFFINITY_MAX_ENTRIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable conversation-based routing affinity.\",\n )\n ttl: int = Field(\n 3600,\n ge=60,\n description=\"TTL for conversation affinity entries in seconds.\",\n )\n max_entries: int = Field(\n 10000,\n ge=100,\n description=\"Maximum number of cached affinity entries.\",\n )\n\n\nclass PluginSettings(BaseModel):\n \"\"\"Gateway plugin system configuration.\n\n Env vars: ``LLMROUTER_PLUGINS``, ``LLMROUTER_PLUGINS_ALLOWLIST``,\n ``LLMROUTER_PLUGINS_ALLOWED_CAPABILITIES``,\n ``LLMROUTER_PLUGINS_FAILURE_MODE``, ``ROUTEIQ_PLUGIN_STARTUP_TIMEOUT``.\n \"\"\"\n\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of plugin module paths to load.\",\n )\n allowlist: Optional[str] = Field(\n None,\n description=\"Comma-separated plugin names allowed to load.\",\n )\n allowed_capabilities: Optional[str] = Field(\n None,\n description=\"Comma-separated capabilities allowed for plugins.\",\n )\n failure_mode: str = Field(\n \"continue\",\n description=\"Plugin failure mode: continue or abort.\",\n )\n startup_timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Maximum seconds for plugin startup.\",\n )\n\n\nclass ManagementSettings(BaseModel):\n \"\"\"Management endpoint middleware configuration.\n\n Env vars: ``ROUTEIQ_MANAGEMENT_RBAC_ENABLED``,\n ``ROUTEIQ_MANAGEMENT_OTEL_ENABLED``.\n \"\"\"\n\n rbac_enabled: bool = Field(\n False,\n description=\"Enable RBAC for LiteLLM management endpoints.\",\n )\n otel_enabled: bool = Field(\n True,\n description=\"Emit OTel span attributes for management operations.\",\n )\n\n\nclass CacheSettings(BaseModel):\n \"\"\"Caching plugin configuration.\n\n Env vars: ``CACHE_ENABLED``, ``CACHE_SEMANTIC_ENABLED``,\n ``CACHE_TTL_SECONDS``, ``CACHE_L1_MAX_SIZE``,\n ``CACHE_SIMILARITY_THRESHOLD``, ``CACHE_EMBEDDING_MODEL``,\n ``CACHE_REDIS_URL``, ``CACHE_MAX_TEMPERATURE``.\n \"\"\"\n\n enabled: bool = Field(False, description=\"Enable the cache plugin.\")\n semantic_enabled: bool = Field(\n False, description=\"Enable semantic (embedding-based) caching.\"\n )\n ttl_seconds: int = Field(3600, ge=1, description=\"Cache entry TTL in seconds.\")\n l1_max_size: int = Field(\n 1000, ge=1, description=\"Max entries in L1 in-process cache.\"\n )\n similarity_threshold: float = Field(\n 0.95,\n ge=0.0,\n le=1.0,\n description=\"Minimum cosine similarity for semantic cache hits.\",\n )\n embedding_model: str = Field(\n \"all-MiniLM-L6-v2\",\n description=\"Sentence-transformer model for embeddings.\",\n )\n redis_url: Optional[str] = Field(\n None,\n description=\"Redis URL for L2 cache (falls back to REDIS_HOST).\",\n )\n max_temperature: float = Field(\n 0.1,\n ge=0.0,\n le=2.0,\n description=\"Max temperature for cacheability.\",\n )\n\n\nclass RouterR1Settings(BaseModel):\n \"\"\"Router-R1 iterative reasoning router configuration.\n\n Implements the Router-R1 concept (NeurIPS 2025) natively using RouteIQ's\n own LLM proxy as both the reasoning engine and routing pool.\n\n Env vars: ``ROUTEIQ_ROUTER_R1_ENABLED``, ``ROUTEIQ_ROUTER_R1_MODEL``,\n ``ROUTEIQ_ROUTER_R1_MAX_ITERATIONS``, ``ROUTEIQ_ROUTER_R1_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the Router-R1 iterative reasoning router.\",\n )\n model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used as the reasoning/routing agent.\",\n )\n max_iterations: int = Field(\n 3,\n ge=1,\n le=10,\n description=\"Maximum reasoning iterations per query.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Timeout in seconds per iteration.\",\n )\n\n\nclass ContextOptimizerSettings(BaseModel):\n \"\"\"Context optimizer plugin configuration.\n\n Reduces LLM input tokens by 30-70% using deterministic, lossless transforms.\n\n Env vars: ``ROUTEIQ_CONTEXT_OPTIMIZE``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_MAX_TURNS``,\n ``ROUTEIQ_CONTEXT_OPTIMIZE_KEEP_LAST``.\n \"\"\"\n\n mode: str = Field(\n \"off\",\n description=\"Optimization mode: off, safe, or aggressive.\",\n )\n max_turns: int = Field(\n 40,\n ge=1,\n description=\"Trim conversations with more turns than this.\",\n )\n keep_last: int = Field(\n 20,\n ge=1,\n description=\"Keep this many recent turns after trimming.\",\n )\n\n\nclass EvaluatorSettings(BaseModel):\n \"\"\"Evaluator plugin configuration.\n\n Env vars: ``ROUTEIQ_EVALUATOR_ENABLED``, ``ROUTEIQ_EVALUATOR_PLUGINS``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable evaluator hooks for post-invocation scoring.\",\n )\n plugins: str = Field(\n \"\",\n description=\"Comma-separated list of evaluator plugin paths.\",\n )\n\n\nclass ContentFilterSettings(BaseModel):\n \"\"\"Content/toxicity filter plugin configuration.\n\n Env vars: ``CONTENT_FILTER_ENABLED``, ``CONTENT_FILTER_THRESHOLD``,\n ``CONTENT_FILTER_ACTION``, ``CONTENT_FILTER_CATEGORIES``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable content/toxicity filtering.\",\n )\n threshold: float = Field(\n 0.7,\n ge=0.0,\n le=1.0,\n description=\"Default score threshold for category violations.\",\n )\n action: str = Field(\n \"block\",\n description=\"Default action: block, warn, or log.\",\n )\n categories: str = Field(\n \"violence,hate_speech,sexual,self_harm,illegal_activity\",\n description=\"Comma-separated list of active filter categories.\",\n )\n\n\nclass AgenticPipelineSettings(BaseModel):\n \"\"\"Agentic multi-round routing pipeline configuration.\n\n Decomposes complex queries into sub-queries, routes independently,\n and aggregates responses.\n\n Env vars: ``ROUTEIQ_AGENTIC_PIPELINE``,\n ``ROUTEIQ_AGENTIC_ORCHESTRATOR_MODEL``,\n ``ROUTEIQ_AGENTIC_COMPLEXITY_THRESHOLD``,\n ``ROUTEIQ_AGENTIC_MAX_SUBQUERIES``,\n ``ROUTEIQ_AGENTIC_PARALLEL``,\n ``ROUTEIQ_AGENTIC_TIMEOUT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the agentic multi-round routing pipeline.\",\n )\n orchestrator_model: str = Field(\n \"gpt-4o-mini\",\n description=\"Model for decompose/aggregate orchestration.\",\n )\n complexity_threshold: float = Field(\n 0.6,\n ge=0.0,\n le=1.0,\n description=\"Queries above this complexity score get decomposed.\",\n )\n max_subqueries: int = Field(\n 4,\n ge=1,\n le=10,\n description=\"Maximum decomposition fan-out.\",\n )\n parallel: bool = Field(\n True,\n description=\"Execute sub-queries in parallel.\",\n )\n timeout: float = Field(\n 30.0,\n ge=1.0,\n description=\"Per sub-query timeout in seconds.\",\n )\n\n\nclass PromptManagementSettings(BaseModel):\n \"\"\"Prompt management & versioning configuration.\n\n Env var: ``ROUTEIQ_PROMPT_MANAGEMENT``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable prompt management and versioning.\",\n )\n\n\nclass CostTrackerSettings(BaseModel):\n \"\"\"Cost tracker plugin configuration.\n\n Env var: ``COST_TRACKER_ENABLED``.\n \"\"\"\n\n enabled: bool = Field(\n True,\n description=\"Enable per-request LLM cost tracking.\",\n )\n\n\nclass SkillsDiscoverySettings(BaseModel):\n \"\"\"Skills discovery plugin configuration.\n\n Env var: ``ROUTEIQ_SKILLS_DIR``.\n \"\"\"\n\n skills_dir: Optional[str] = Field(\n None,\n description=\"Directory to load skills from (default: ./skills or ./docs/skills).\",\n )\n\n\nclass EvalPipelineSettings(BaseModel):\n \"\"\"Evaluation pipeline configuration.\n\n Closes the feedback loop between routing decisions and quality outcomes.\n When enabled, a fraction of requests are evaluated by an LLM-as-judge\n and the resulting quality scores feed back into routing strategies.\n\n Env vars: ``ROUTEIQ_EVAL_PIPELINE``, ``ROUTEIQ_EVAL_SAMPLE_RATE``,\n ``ROUTEIQ_EVAL_JUDGE_MODEL``, ``ROUTEIQ_EVAL_BATCH_SIZE``,\n ``ROUTEIQ_EVAL_FEEDBACK_INTERVAL``.\n \"\"\"\n\n enabled: bool = Field(\n False,\n description=\"Enable the evaluation feedback loop pipeline.\",\n )\n sample_rate: float = Field(\n 0.1,\n ge=0.0,\n le=1.0,\n description=\"Fraction of requests to evaluate (0.0-1.0).\",\n )\n judge_model: str = Field(\n \"gpt-4o-mini\",\n description=\"LLM model used for LLM-as-judge evaluations.\",\n )\n batch_size: int = Field(\n 10,\n ge=1,\n description=\"Number of samples per evaluation batch.\",\n )\n feedback_interval: int = Field(\n 300,\n ge=10,\n description=\"Seconds between routing feedback updates.\",\n )\n\n\nclass HASettings(BaseModel):\n \"\"\"High-availability and leader election configuration.\n\n Env vars: ``LLMROUTER_HA_MODE``, ``ROUTEIQ_LEADER_MIGRATIONS``,\n ``ROUTEIQ_LEADER_ELECTION_BACKEND``.\n \"\"\"\n\n mode: str = Field(\n \"\",\n description=\"HA mode (empty = disabled, set to 'leader-election').\",\n )\n leader_migrations: bool = Field(\n False,\n description=\"Run DB migrations via leader election at startup.\",\n )\n leader_election_backend: str = Field(\n \"\",\n description=(\n \"Explicit leader election backend override: \"\n \"kubernetes, redis, postgres, none. \"\n \"When empty, auto-detected from environment.\"\n ),\n )\n\n\n# ============================================================================\n# Root Settings\n# ============================================================================\n\n\nclass GatewaySettings(BaseSettings):\n \"\"\"Root configuration for RouteIQ Gateway.\n\n All settings can be overridden via environment variables.\n Nested models use underscore-separated prefixes\n (e.g. ``ROUTEIQ_REDIS_HOST``, ``ROUTEIQ_OTEL_ENABLED``).\n\n LiteLLM pass-through variables (``LITELLM_*``) use\n ``validation_alias`` so the existing env var names are preserved.\n \"\"\"\n\n model_config = SettingsConfigDict(\n env_prefix=\"ROUTEIQ_\",\n env_nested_delimiter=\"__\",\n case_sensitive=False,\n extra=\"ignore\",\n )\n\n # ------------------------------------------------------------------\n # Core gateway settings\n # ------------------------------------------------------------------\n\n config_path: Optional[str] = Field(\n None,\n description=\"Path to YAML config file.\",\n json_schema_extra={\"env\": \"LITELLM_CONFIG_PATH\"},\n )\n port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"HTTP port to listen on.\",\n )\n host: str = Field(\n \"0.0.0.0\",\n description=\"Host to bind to.\",\n )\n workers: int = Field(\n 1,\n ge=1,\n description=(\n \"Uvicorn worker count. Multi-worker requires \"\n \"plugin strategy mode (the default).\"\n ),\n )\n env: str = Field(\n \"production\",\n description=\"Environment: production, staging, development.\",\n )\n debug: bool = Field(\n False,\n description=\"Enable debug mode.\",\n )\n admin_ui_enabled: bool = Field(\n False,\n description=\"Enable admin UI at /ui/.\",\n )\n admin_ui_external_url: Optional[str] = Field(\n None,\n description=(\n \"External URL where the UI is hosted (for CORS). \"\n \"Leave empty when UI is embedded in the gateway container. \"\n \"Set to the UI origin (e.g. 'https://ui.routeiq.example.com') \"\n \"for disaggregated deployments so the gateway auto-adds it \"\n \"to CORS allowed origins.\"\n ),\n )\n skip_env_validation: bool = Field(\n False,\n description=\"Skip advisory env validation at startup.\",\n )\n own_app: bool = Field(\n True,\n description=(\n \"Use RouteIQ-owned FastAPI app (ADR-0012). \"\n \"Set to false to fall back to legacy mode where LiteLLM \"\n \"owns the FastAPI instance.\"\n ),\n )\n\n # ------------------------------------------------------------------\n # LiteLLM pass-through (preserve existing env var names)\n # ------------------------------------------------------------------\n\n litellm_master_key: Optional[str] = Field(\n None,\n description=\"Master API key for admin access (LITELLM_MASTER_KEY).\",\n )\n litellm_config_path: Optional[str] = Field(\n None,\n description=\"Default config path (LITELLM_CONFIG_PATH).\",\n )\n litellm_port: int = Field(\n 4000,\n ge=1,\n le=65535,\n description=\"LiteLLM proxy port (LITELLM_PORT).\",\n )\n litellm_host: str = Field(\n \"0.0.0.0\",\n description=\"LiteLLM proxy host (LITELLM_HOST).\",\n )\n litellm_debug: bool = Field(\n False,\n description=\"LiteLLM debug mode (LITELLM_DEBUG).\",\n )\n\n # ------------------------------------------------------------------\n # LLMRouter settings (LLMROUTER_* prefix)\n # ------------------------------------------------------------------\n\n llmrouter_router_callback_enabled: bool = Field(\n True,\n description=\"Emit router decision telemetry (TG4.1).\",\n )\n\n # ------------------------------------------------------------------\n # Sub-configurations (nested models)\n # ------------------------------------------------------------------\n\n redis: RedisSettings = Field(\n default_factory=RedisSettings,\n description=\"Redis connection settings.\",\n )\n postgres: PostgresSettings = Field(\n default_factory=PostgresSettings,\n description=\"PostgreSQL connection settings.\",\n )\n otel: OTelSettings = Field(\n default_factory=OTelSettings,\n description=\"OpenTelemetry observability settings.\",\n )\n oidc: OIDCSettings = Field(\n default_factory=OIDCSettings,\n description=\"OIDC / SSO integration settings.\",\n )\n routing: RoutingSettings = Field(\n default_factory=RoutingSettings,\n description=\"ML routing strategy settings.\",\n )\n security: SecuritySettings = Field(\n default_factory=SecuritySettings,\n description=\"Security and access control settings.\",\n )\n policy: PolicySettings = Field(\n default_factory=PolicySettings,\n description=\"Policy engine settings.\",\n )\n audit: AuditSettings = Field(\n default_factory=AuditSettings,\n description=\"Audit logging settings.\",\n )\n quota: QuotaSettings = Field(\n default_factory=QuotaSettings,\n description=\"Quota enforcement settings.\",\n )\n mcp: MCPSettings = Field(\n default_factory=MCPSettings,\n description=\"MCP gateway settings.\",\n )\n a2a: A2ASettings = Field(\n default_factory=A2ASettings,\n description=\"A2A gateway settings.\",\n )\n resilience: ResilienceSettings = Field(\n default_factory=ResilienceSettings,\n description=\"Backpressure and resilience settings.\",\n )\n config_sync: ConfigSyncSettings = Field(\n default_factory=ConfigSyncSettings,\n description=\"Remote config sync settings.\",\n )\n http_client: HTTPClientSettings = Field(\n default_factory=HTTPClientSettings,\n description=\"Shared HTTP client pool settings.\",\n )\n ssrf: SSRFSettings = Field(\n default_factory=SSRFSettings,\n description=\"SSRF protection settings.\",\n )\n conversation_affinity: ConversationAffinitySettings = Field(\n default_factory=ConversationAffinitySettings,\n description=\"Conversation routing affinity settings.\",\n )\n plugins: PluginSettings = Field(\n default_factory=PluginSettings,\n description=\"Plugin system settings.\",\n )\n management: ManagementSettings = Field(\n default_factory=ManagementSettings,\n description=\"Management middleware settings.\",\n )\n cache: CacheSettings = Field(\n default_factory=CacheSettings,\n description=\"Caching plugin settings.\",\n )\n ha: HASettings = Field(\n default_factory=HASettings,\n description=\"High-availability settings.\",\n )\n eval_pipeline: EvalPipelineSettings = Field(\n default_factory=EvalPipelineSettings,\n description=\"Evaluation feedback loop pipeline settings.\",\n )\n router_r1: RouterR1Settings = Field(\n default_factory=RouterR1Settings,\n description=\"Router-R1 iterative reasoning router settings.\",\n )\n context_optimizer: ContextOptimizerSettings = Field(\n default_factory=ContextOptimizerSettings,\n description=\"Context optimizer plugin settings.\",\n )\n evaluator: EvaluatorSettings = Field(\n default_factory=EvaluatorSettings,\n description=\"Evaluator plugin settings.\",\n )\n content_filter: ContentFilterSettings = Field(\n default_factory=ContentFilterSettings,\n description=\"Content/toxicity filter settings.\",\n )\n agentic_pipeline: AgenticPipelineSettings = Field(\n default_factory=AgenticPipelineSettings,\n description=\"Agentic multi-round routing pipeline settings.\",\n )\n prompt_management: PromptManagementSettings = Field(\n default_factory=PromptManagementSettings,\n description=\"Prompt management and versioning settings.\",\n )\n cost_tracker: CostTrackerSettings = Field(\n default_factory=CostTrackerSettings,\n description=\"Cost tracker plugin settings.\",\n )\n skills_discovery: SkillsDiscoverySettings = Field(\n default_factory=SkillsDiscoverySettings,\n description=\"Skills discovery plugin settings.\",\n )\n\n # ------------------------------------------------------------------\n # Validators\n # ------------------------------------------------------------------\n\n @field_validator(\"litellm_master_key\")\n @classmethod\n def _validate_master_key(cls, v: Optional[str]) -> Optional[str]:\n \"\"\"Warn if the master key looks like a placeholder.\"\"\"\n if v and v.lower() in (\"sk-1234\", \"changeme\", \"test\", \"your-key-here\"):\n warnings.warn(\n \"LITELLM_MASTER_KEY appears to be a placeholder value — \"\n \"admin endpoints may be unprotected\",\n stacklevel=2,\n )\n return v\n\n @model_validator(mode=\"after\")\n def _cross_validate(self) -> \"GatewaySettings\":\n \"\"\"Cross-field validation after all fields are populated.\"\"\"\n return self\n\n # ------------------------------------------------------------------\n # Convenience properties\n # ------------------------------------------------------------------\n\n @property\n def cors_origins_list(self) -> list[str]:\n \"\"\"Parse CORS origins string into a list.\"\"\"\n return [o.strip() for o in self.security.cors_origins.split(\",\")]\n\n @property\n def admin_api_keys_set(self) -> set[str]:\n \"\"\"Parse admin API keys into a set (includes both fields).\"\"\"\n keys: set[str] = set()\n if self.security.admin_api_keys:\n for key in self.security.admin_api_keys.split(\",\"):\n key = key.strip()\n if key:\n keys.add(key)\n if self.security.admin_api_key:\n keys.add(self.security.admin_api_key.strip())\n return keys\n\n @property\n def is_production(self) -> bool:\n \"\"\"Check if running in production environment.\"\"\"\n return self.env.lower() == \"production\"\n\n @property\n def redis_configured(self) -> bool:\n \"\"\"Check if Redis is configured (host is set).\"\"\"\n return self.redis.host is not None\n\n @property\n def postgres_configured(self) -> bool:\n \"\"\"Check if PostgreSQL is configured (URL is set).\"\"\"\n return self.postgres.url is not None\n\n # ------------------------------------------------------------------\n # Legacy env-var compatibility helpers\n # ------------------------------------------------------------------\n\n @classmethod\n def settings_customise_sources(\n cls,\n settings_cls: type[BaseSettings],\n init_settings: Any,\n env_settings: Any,\n dotenv_settings: Any,\n file_secret_settings: Any,\n ) -> tuple[Any, ...]:\n \"\"\"Customise settings sources to include legacy env var mappings.\n\n Priority order (highest first):\n 1. Init kwargs (programmatic overrides)\n 2. Environment variables (with ROUTEIQ_ prefix)\n 3. Dotenv files\n 4. File secrets\n \"\"\"\n return (\n init_settings,\n env_settings,\n dotenv_settings,\n file_secret_settings,\n )\n\n\n# ============================================================================\n# Singleton\n# ============================================================================\n\n_settings: Optional[GatewaySettings] = None\n\n\ndef get_settings(**overrides: Any) -> GatewaySettings:\n \"\"\"Get or create the settings singleton.\n\n On first call, creates the ``GatewaySettings`` instance by reading\n environment variables. Subsequent calls return the cached instance.\n\n Args:\n **overrides: Keyword arguments passed to ``GatewaySettings()`` on\n first creation only (useful for testing).\n\n Returns:\n The gateway settings singleton.\n \"\"\"\n global _settings\n if _settings is None:\n _settings = GatewaySettings(**overrides)\n return _settings\n\n\ndef reset_settings() -> None:\n \"\"\"Reset the settings singleton.\n\n **Must** be called in test fixtures (``autouse=True``) to prevent\n cross-test contamination from cached settings.\n \"\"\"\n global _settings\n _settings = None\n"}}}} |