4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
679 lines
26 KiB
Python
679 lines
26 KiB
Python
"""Global application configuration.
|
|
|
|
Clerk JWT configuration for both development and production environments.
|
|
These are public endpoints and issuer URLs, not secrets.
|
|
"""
|
|
|
|
import os
|
|
from collections.abc import Sequence
|
|
from dataclasses import dataclass
|
|
from difflib import get_close_matches
|
|
from enum import Enum
|
|
from typing import Literal
|
|
|
|
from pydantic import Field, ValidationError, field_validator, model_validator
|
|
|
|
from config.llm_auth.auth_method import (
|
|
LLM_AUTH_METHOD_ENV,
|
|
effective_llm_provider,
|
|
get_configured_llm_auth_method,
|
|
)
|
|
from config.llm_auth.credentials import status as credential_status
|
|
from config.llm_auth.provider_catalog import (
|
|
API_KEY_PROVIDER_ENVS,
|
|
SUPPORTED_PROVIDER_VALUES,
|
|
)
|
|
from config.local_env import bootstrap_opensre_env
|
|
from config.strict_config import StrictConfigModel
|
|
|
|
|
|
class LLMModelConfig(StrictConfigModel):
|
|
"""Configuration for an LLM provider's model variants.
|
|
|
|
Three tiers, ordered by capability/cost:
|
|
- ``reasoning_model`` — highest-capability model used for root-cause
|
|
diagnosis and other deep-reasoning steps (e.g. Claude Opus, GPT-5).
|
|
- ``classification_model`` — mid-tier model for tasks that need more
|
|
reasoning than a fast toolcall model but don't justify reasoning cost
|
|
(e.g. interactive-shell intent classification). Sonnet for Anthropic.
|
|
- ``toolcall_model`` — lightweight, low-latency model for simple tool
|
|
selection / action planning (e.g. Claude Haiku, GPT-5 mini).
|
|
"""
|
|
|
|
reasoning_model: str
|
|
classification_model: str
|
|
toolcall_model: str
|
|
max_tokens: int
|
|
|
|
|
|
class Environment(Enum):
|
|
"""Application environment."""
|
|
|
|
DEVELOPMENT = "development"
|
|
PRODUCTION = "production"
|
|
|
|
|
|
class ClerkConfig(StrictConfigModel):
|
|
"""Clerk JWT configuration for a specific environment."""
|
|
|
|
jwks_url: str
|
|
issuer: str
|
|
|
|
|
|
CLERK_CONFIG_DEV = ClerkConfig(
|
|
jwks_url="https://superb-jackal-75.clerk.accounts.dev/.well-known/jwks.json",
|
|
issuer="https://superb-jackal-75.clerk.accounts.dev",
|
|
)
|
|
|
|
CLERK_CONFIG_PROD = ClerkConfig(
|
|
jwks_url="https://clerk.tracer.cloud/.well-known/jwks.json",
|
|
issuer="https://clerk.tracer.cloud",
|
|
)
|
|
|
|
|
|
def get_environment() -> Environment:
|
|
"""Get current environment from ENV variable.
|
|
|
|
Returns:
|
|
Environment enum value based on ENV variable.
|
|
Defaults to DEVELOPMENT if not set or unrecognized.
|
|
"""
|
|
env_value = os.getenv("ENV", "development").lower()
|
|
if env_value in ("production", "prod"):
|
|
return Environment.PRODUCTION
|
|
return Environment.DEVELOPMENT
|
|
|
|
|
|
# JWT Configuration
|
|
JWT_ALGORITHM = "RS256"
|
|
JWKS_CACHE_TTL_SECONDS = 3600
|
|
|
|
# LLM Model Constants
|
|
DEFAULT_MAX_TOKENS = 4096
|
|
|
|
# Anthropic model constants
|
|
ANTHROPIC_REASONING_MODEL = "claude-opus-4-7"
|
|
ANTHROPIC_CLASSIFICATION_MODEL = "claude-sonnet-4-6"
|
|
ANTHROPIC_TOOLCALL_MODEL = "claude-haiku-4-5-20251001"
|
|
|
|
# OpenAI model constants
|
|
# Default to GPT-5.4 mini for both reasoning and toolcall paths; override via
|
|
# OPENAI_REASONING_MODEL / OPENAI_TOOLCALL_MODEL when needed.
|
|
OPENAI_REASONING_MODEL = "gpt-5.4-mini"
|
|
# Mid-tier mirrors the toolcall (mini) model by default — OpenAI's mini sits
|
|
# between full and nano, which matches the "Sonnet-equivalent" classification
|
|
# tier well enough; override via OPENAI_CLASSIFICATION_MODEL when needed.
|
|
OPENAI_CLASSIFICATION_MODEL = "gpt-5.4-mini"
|
|
OPENAI_TOOLCALL_MODEL = "gpt-5.4-mini"
|
|
|
|
# OpenRouter model constants
|
|
OPENROUTER_REASONING_MODEL = "openrouter/auto"
|
|
OPENROUTER_CLASSIFICATION_MODEL = "openrouter/auto"
|
|
OPENROUTER_TOOLCALL_MODEL = "openrouter/auto"
|
|
|
|
# DeepSeek model constants
|
|
DEEPSEEK_REASONING_MODEL = "deepseek-v4-pro"
|
|
DEEPSEEK_CLASSIFICATION_MODEL = "deepseek-v4-flash"
|
|
DEEPSEEK_TOOLCALL_MODEL = "deepseek-v4-flash"
|
|
|
|
# Gemini model constants (Google AI preview IDs; OpenAI-compatible endpoint)
|
|
# UNVERIFIED PLACEHOLDER — gemini-3.1-pro-preview / gemini-3.1-flash-lite-preview are
|
|
# forward-looking IDs that may not yet exist. Override via GEMINI_REASONING_MODEL env var.
|
|
GEMINI_REASONING_MODEL = "gemini-3.1-pro-preview"
|
|
GEMINI_CLASSIFICATION_MODEL = "gemini-3-flash-preview"
|
|
GEMINI_TOOLCALL_MODEL = "gemini-3.1-flash-lite-preview"
|
|
|
|
# NVIDIA NIM model constants
|
|
# Verified safe defaults from the NVIDIA API Catalog (build.nvidia.com).
|
|
# Override via NVIDIA_REASONING_MODEL, NVIDIA_TOOLCALL_MODEL, or NVIDIA_MODEL env vars.
|
|
NVIDIA_REASONING_MODEL = "meta/llama-3.1-405b-instruct"
|
|
NVIDIA_CLASSIFICATION_MODEL = "meta/llama-3.1-70b-instruct"
|
|
NVIDIA_TOOLCALL_MODEL = "meta/llama-3.1-8b-instruct"
|
|
|
|
# MiniMax model constants
|
|
MINIMAX_REASONING_MODEL = "MiniMax-M3"
|
|
MINIMAX_CLASSIFICATION_MODEL = "MiniMax-M2.7-highspeed"
|
|
MINIMAX_TOOLCALL_MODEL = "MiniMax-M2.7-highspeed"
|
|
|
|
# Groq model constants
|
|
GROQ_REASONING_MODEL = "llama-3.3-70b-versatile"
|
|
GROQ_CLASSIFICATION_MODEL = "llama-3.3-70b-versatile"
|
|
GROQ_TOOLCALL_MODEL = "llama-3.1-8b-instant"
|
|
|
|
# Azure OpenAI deployment-name defaults (must match your Azure deployment names).
|
|
AZURE_OPENAI_REASONING_MODEL = "gpt-5.4-mini"
|
|
AZURE_OPENAI_CLASSIFICATION_MODEL = "gpt-5.4-mini"
|
|
AZURE_OPENAI_TOOLCALL_MODEL = "gpt-5.4-mini"
|
|
DEFAULT_AZURE_OPENAI_API_VERSION = "2024-10-21"
|
|
|
|
# Base URLs for OpenAI-compatible providers
|
|
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
|
DEEPSEEK_BASE_URL = "https://api.deepseek.com" # no /v1 — DeepSeek serves the OpenAI-compatible API at the root path
|
|
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
|
NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1"
|
|
MINIMAX_BASE_URL = "https://api.minimax.io/v1"
|
|
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
|
|
|
|
# Amazon Bedrock model constants (US cross-region inference profile IDs)
|
|
BEDROCK_REASONING_MODEL = "us.anthropic.claude-sonnet-4-6"
|
|
BEDROCK_CLASSIFICATION_MODEL = "us.anthropic.claude-sonnet-4-6"
|
|
BEDROCK_TOOLCALL_MODEL = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
|
|
|
|
# Ollama local model constants
|
|
DEFAULT_OLLAMA_MODEL = "llama3.2"
|
|
DEFAULT_OLLAMA_HOST = "http://localhost:11434"
|
|
|
|
LLMProvider = Literal[
|
|
"anthropic",
|
|
"openai",
|
|
"openrouter",
|
|
"deepseek",
|
|
"gemini",
|
|
"nvidia",
|
|
"ollama",
|
|
"bedrock",
|
|
"minimax",
|
|
"groq",
|
|
"azure-openai",
|
|
"codex",
|
|
"cursor",
|
|
"claude-code",
|
|
"gemini-cli",
|
|
"antigravity-cli",
|
|
"opencode",
|
|
"kimi",
|
|
"copilot",
|
|
"grok-cli",
|
|
"pi",
|
|
]
|
|
|
|
LLM_PROVIDER_API_KEY_ENVS = API_KEY_PROVIDER_ENVS
|
|
|
|
# Runtime identifiers for ``LLMProvider`` members. Branch on these instead of
|
|
# bare string literals when routing on the active provider.
|
|
PROVIDER_ANTHROPIC: LLMProvider = "anthropic"
|
|
PROVIDER_OPENAI: LLMProvider = "openai"
|
|
PROVIDER_BEDROCK: LLMProvider = "bedrock"
|
|
PROVIDER_OLLAMA: LLMProvider = "ollama"
|
|
|
|
|
|
def get_configured_llm_provider() -> str:
|
|
"""Return the active LLM provider from env/project .env."""
|
|
bootstrap_opensre_env(override=False)
|
|
return os.getenv("LLM_PROVIDER", "anthropic").strip().lower() or "anthropic"
|
|
|
|
|
|
def get_llm_provider_api_key_env(provider: str | None = None) -> str | None:
|
|
"""Return the API-key env var required by an LLM provider, if any."""
|
|
provider_name = (provider or get_configured_llm_provider()).strip().lower()
|
|
auth_method = get_configured_llm_auth_method(provider_name)
|
|
if effective_llm_provider(provider_name, auth_method) != provider_name:
|
|
return None
|
|
return LLM_PROVIDER_API_KEY_ENVS.get(provider_name)
|
|
|
|
|
|
def get_llm_provider_api_key(provider: str | None = None) -> tuple[str | None, str]:
|
|
"""Return an env API key only; Keychain reads are request-scoped now."""
|
|
env_var = get_llm_provider_api_key_env(provider)
|
|
if env_var is None:
|
|
return None, ""
|
|
return env_var, os.getenv(env_var, "").strip()
|
|
|
|
|
|
def _llm_api_key_payload(provider: str) -> dict[str, str]:
|
|
"""Return no secrets; runtime resolves credentials request-time."""
|
|
_ = provider
|
|
return {}
|
|
|
|
|
|
def _llm_settings_env_payload(provider: str) -> dict[str, object]:
|
|
"""Build the raw env-backed payload used to validate LLM settings."""
|
|
return {
|
|
"provider": provider,
|
|
**_llm_api_key_payload(provider),
|
|
"anthropic_reasoning_model": os.getenv(
|
|
"ANTHROPIC_REASONING_MODEL", ANTHROPIC_REASONING_MODEL
|
|
).strip()
|
|
or ANTHROPIC_REASONING_MODEL,
|
|
"anthropic_classification_model": os.getenv(
|
|
"ANTHROPIC_CLASSIFICATION_MODEL", ANTHROPIC_CLASSIFICATION_MODEL
|
|
).strip()
|
|
or ANTHROPIC_CLASSIFICATION_MODEL,
|
|
"anthropic_toolcall_model": os.getenv(
|
|
"ANTHROPIC_TOOLCALL_MODEL", ANTHROPIC_TOOLCALL_MODEL
|
|
).strip()
|
|
or ANTHROPIC_TOOLCALL_MODEL,
|
|
"openai_reasoning_model": os.getenv(
|
|
"OPENAI_REASONING_MODEL", OPENAI_REASONING_MODEL
|
|
).strip()
|
|
or OPENAI_REASONING_MODEL,
|
|
"openai_classification_model": os.getenv(
|
|
"OPENAI_CLASSIFICATION_MODEL", OPENAI_CLASSIFICATION_MODEL
|
|
).strip()
|
|
or OPENAI_CLASSIFICATION_MODEL,
|
|
"openai_toolcall_model": os.getenv("OPENAI_TOOLCALL_MODEL", OPENAI_TOOLCALL_MODEL).strip()
|
|
or OPENAI_TOOLCALL_MODEL,
|
|
"openrouter_reasoning_model": os.getenv(
|
|
"OPENROUTER_REASONING_MODEL",
|
|
os.getenv("OPENROUTER_MODEL", OPENROUTER_REASONING_MODEL),
|
|
).strip()
|
|
or OPENROUTER_REASONING_MODEL,
|
|
"openrouter_classification_model": os.getenv(
|
|
"OPENROUTER_CLASSIFICATION_MODEL",
|
|
os.getenv("OPENROUTER_MODEL", OPENROUTER_CLASSIFICATION_MODEL),
|
|
).strip()
|
|
or OPENROUTER_CLASSIFICATION_MODEL,
|
|
"openrouter_toolcall_model": os.getenv(
|
|
"OPENROUTER_TOOLCALL_MODEL",
|
|
os.getenv("OPENROUTER_MODEL", OPENROUTER_TOOLCALL_MODEL),
|
|
).strip()
|
|
or OPENROUTER_TOOLCALL_MODEL,
|
|
"deepseek_reasoning_model": os.getenv(
|
|
"DEEPSEEK_REASONING_MODEL",
|
|
os.getenv("DEEPSEEK_MODEL", DEEPSEEK_REASONING_MODEL),
|
|
).strip()
|
|
or DEEPSEEK_REASONING_MODEL,
|
|
"deepseek_classification_model": os.getenv(
|
|
"DEEPSEEK_CLASSIFICATION_MODEL",
|
|
os.getenv("DEEPSEEK_MODEL", DEEPSEEK_CLASSIFICATION_MODEL),
|
|
).strip()
|
|
or DEEPSEEK_CLASSIFICATION_MODEL,
|
|
"deepseek_toolcall_model": os.getenv(
|
|
"DEEPSEEK_TOOLCALL_MODEL",
|
|
os.getenv("DEEPSEEK_MODEL", DEEPSEEK_TOOLCALL_MODEL),
|
|
).strip()
|
|
or DEEPSEEK_TOOLCALL_MODEL,
|
|
"gemini_reasoning_model": os.getenv(
|
|
"GEMINI_REASONING_MODEL",
|
|
os.getenv("GEMINI_MODEL", GEMINI_REASONING_MODEL),
|
|
).strip()
|
|
or GEMINI_REASONING_MODEL,
|
|
"gemini_classification_model": os.getenv(
|
|
"GEMINI_CLASSIFICATION_MODEL",
|
|
os.getenv("GEMINI_MODEL", GEMINI_CLASSIFICATION_MODEL),
|
|
).strip()
|
|
or GEMINI_CLASSIFICATION_MODEL,
|
|
"gemini_toolcall_model": os.getenv(
|
|
"GEMINI_TOOLCALL_MODEL",
|
|
os.getenv("GEMINI_MODEL", GEMINI_TOOLCALL_MODEL),
|
|
).strip()
|
|
or GEMINI_TOOLCALL_MODEL,
|
|
"nvidia_reasoning_model": os.getenv(
|
|
"NVIDIA_REASONING_MODEL",
|
|
os.getenv("NVIDIA_MODEL", NVIDIA_REASONING_MODEL),
|
|
).strip()
|
|
or NVIDIA_REASONING_MODEL,
|
|
"nvidia_classification_model": os.getenv(
|
|
"NVIDIA_CLASSIFICATION_MODEL",
|
|
os.getenv("NVIDIA_MODEL", NVIDIA_CLASSIFICATION_MODEL),
|
|
).strip()
|
|
or NVIDIA_CLASSIFICATION_MODEL,
|
|
"nvidia_toolcall_model": os.getenv(
|
|
"NVIDIA_TOOLCALL_MODEL",
|
|
os.getenv("NVIDIA_MODEL", NVIDIA_TOOLCALL_MODEL),
|
|
).strip()
|
|
or NVIDIA_TOOLCALL_MODEL,
|
|
"minimax_reasoning_model": os.getenv(
|
|
"MINIMAX_REASONING_MODEL",
|
|
os.getenv("MINIMAX_MODEL", MINIMAX_REASONING_MODEL),
|
|
).strip()
|
|
or MINIMAX_REASONING_MODEL,
|
|
"minimax_classification_model": os.getenv(
|
|
"MINIMAX_CLASSIFICATION_MODEL",
|
|
os.getenv("MINIMAX_MODEL", MINIMAX_CLASSIFICATION_MODEL),
|
|
).strip()
|
|
or MINIMAX_CLASSIFICATION_MODEL,
|
|
"minimax_toolcall_model": os.getenv(
|
|
"MINIMAX_TOOLCALL_MODEL",
|
|
os.getenv("MINIMAX_MODEL", MINIMAX_TOOLCALL_MODEL),
|
|
).strip()
|
|
or MINIMAX_TOOLCALL_MODEL,
|
|
"groq_reasoning_model": os.getenv(
|
|
"GROQ_REASONING_MODEL",
|
|
os.getenv("GROQ_MODEL", GROQ_REASONING_MODEL),
|
|
).strip()
|
|
or GROQ_REASONING_MODEL,
|
|
"groq_classification_model": os.getenv(
|
|
"GROQ_CLASSIFICATION_MODEL",
|
|
os.getenv("GROQ_MODEL", GROQ_CLASSIFICATION_MODEL),
|
|
).strip()
|
|
or GROQ_CLASSIFICATION_MODEL,
|
|
"groq_toolcall_model": os.getenv(
|
|
"GROQ_TOOLCALL_MODEL",
|
|
os.getenv("GROQ_MODEL", GROQ_TOOLCALL_MODEL),
|
|
).strip()
|
|
or GROQ_TOOLCALL_MODEL,
|
|
"azure_openai_base_url": os.getenv("AZURE_OPENAI_BASE_URL", "").strip(),
|
|
"azure_openai_api_version": os.getenv(
|
|
"AZURE_OPENAI_API_VERSION", DEFAULT_AZURE_OPENAI_API_VERSION
|
|
).strip()
|
|
or DEFAULT_AZURE_OPENAI_API_VERSION,
|
|
"azure_openai_reasoning_model": os.getenv(
|
|
"AZURE_OPENAI_REASONING_MODEL",
|
|
os.getenv("AZURE_OPENAI_MODEL", AZURE_OPENAI_REASONING_MODEL),
|
|
).strip()
|
|
or AZURE_OPENAI_REASONING_MODEL,
|
|
"azure_openai_classification_model": os.getenv(
|
|
"AZURE_OPENAI_CLASSIFICATION_MODEL",
|
|
os.getenv("AZURE_OPENAI_MODEL", AZURE_OPENAI_CLASSIFICATION_MODEL),
|
|
).strip()
|
|
or AZURE_OPENAI_CLASSIFICATION_MODEL,
|
|
"azure_openai_toolcall_model": os.getenv(
|
|
"AZURE_OPENAI_TOOLCALL_MODEL",
|
|
os.getenv("AZURE_OPENAI_MODEL", AZURE_OPENAI_TOOLCALL_MODEL),
|
|
).strip()
|
|
or AZURE_OPENAI_TOOLCALL_MODEL,
|
|
"bedrock_reasoning_model": os.getenv(
|
|
"BEDROCK_REASONING_MODEL", BEDROCK_REASONING_MODEL
|
|
).strip()
|
|
or BEDROCK_REASONING_MODEL,
|
|
"bedrock_classification_model": os.getenv(
|
|
"BEDROCK_CLASSIFICATION_MODEL", BEDROCK_CLASSIFICATION_MODEL
|
|
).strip()
|
|
or BEDROCK_CLASSIFICATION_MODEL,
|
|
"bedrock_toolcall_model": os.getenv(
|
|
"BEDROCK_TOOLCALL_MODEL", BEDROCK_TOOLCALL_MODEL
|
|
).strip()
|
|
or BEDROCK_TOOLCALL_MODEL,
|
|
"ollama_model": os.getenv("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL).strip()
|
|
or DEFAULT_OLLAMA_MODEL,
|
|
"ollama_host": os.getenv("OLLAMA_HOST", DEFAULT_OLLAMA_HOST).strip() or DEFAULT_OLLAMA_HOST,
|
|
"max_tokens": os.getenv("LLM_MAX_TOKENS", str(DEFAULT_MAX_TOKENS)),
|
|
}
|
|
|
|
|
|
class LLMSettings(StrictConfigModel):
|
|
"""Strict runtime configuration for selecting and authenticating an LLM provider."""
|
|
|
|
provider: LLMProvider = "anthropic"
|
|
anthropic_api_key: str = ""
|
|
openai_api_key: str = ""
|
|
openrouter_api_key: str = ""
|
|
deepseek_api_key: str = ""
|
|
gemini_api_key: str = ""
|
|
nvidia_api_key: str = ""
|
|
minimax_api_key: str = ""
|
|
groq_api_key: str = ""
|
|
azure_openai_api_key: str = ""
|
|
azure_openai_base_url: str = ""
|
|
azure_openai_api_version: str = DEFAULT_AZURE_OPENAI_API_VERSION
|
|
ollama_model: str = DEFAULT_OLLAMA_MODEL
|
|
ollama_host: str = DEFAULT_OLLAMA_HOST
|
|
anthropic_reasoning_model: str = ANTHROPIC_REASONING_MODEL
|
|
anthropic_classification_model: str = ANTHROPIC_CLASSIFICATION_MODEL
|
|
anthropic_toolcall_model: str = ANTHROPIC_TOOLCALL_MODEL
|
|
openai_reasoning_model: str = OPENAI_REASONING_MODEL
|
|
openai_classification_model: str = OPENAI_CLASSIFICATION_MODEL
|
|
openai_toolcall_model: str = OPENAI_TOOLCALL_MODEL
|
|
openrouter_reasoning_model: str = OPENROUTER_REASONING_MODEL
|
|
openrouter_classification_model: str = OPENROUTER_CLASSIFICATION_MODEL
|
|
openrouter_toolcall_model: str = OPENROUTER_TOOLCALL_MODEL
|
|
deepseek_reasoning_model: str = DEEPSEEK_REASONING_MODEL
|
|
deepseek_classification_model: str = DEEPSEEK_CLASSIFICATION_MODEL
|
|
deepseek_toolcall_model: str = DEEPSEEK_TOOLCALL_MODEL
|
|
gemini_reasoning_model: str = GEMINI_REASONING_MODEL
|
|
gemini_classification_model: str = GEMINI_CLASSIFICATION_MODEL
|
|
gemini_toolcall_model: str = GEMINI_TOOLCALL_MODEL
|
|
nvidia_reasoning_model: str = NVIDIA_REASONING_MODEL
|
|
nvidia_classification_model: str = NVIDIA_CLASSIFICATION_MODEL
|
|
nvidia_toolcall_model: str = NVIDIA_TOOLCALL_MODEL
|
|
minimax_reasoning_model: str = MINIMAX_REASONING_MODEL
|
|
minimax_classification_model: str = MINIMAX_CLASSIFICATION_MODEL
|
|
minimax_toolcall_model: str = MINIMAX_TOOLCALL_MODEL
|
|
groq_reasoning_model: str = GROQ_REASONING_MODEL
|
|
groq_classification_model: str = GROQ_CLASSIFICATION_MODEL
|
|
groq_toolcall_model: str = GROQ_TOOLCALL_MODEL
|
|
azure_openai_reasoning_model: str = AZURE_OPENAI_REASONING_MODEL
|
|
azure_openai_classification_model: str = AZURE_OPENAI_CLASSIFICATION_MODEL
|
|
azure_openai_toolcall_model: str = AZURE_OPENAI_TOOLCALL_MODEL
|
|
bedrock_reasoning_model: str = BEDROCK_REASONING_MODEL
|
|
bedrock_classification_model: str = BEDROCK_CLASSIFICATION_MODEL
|
|
bedrock_toolcall_model: str = BEDROCK_TOOLCALL_MODEL
|
|
max_tokens: int = Field(default=DEFAULT_MAX_TOKENS, gt=0)
|
|
|
|
@field_validator("ollama_host", mode="before")
|
|
@classmethod
|
|
def _normalize_ollama_host(cls, value: object) -> str:
|
|
host = str(value or DEFAULT_OLLAMA_HOST).strip() or DEFAULT_OLLAMA_HOST
|
|
if not host.startswith(("http://", "https://")):
|
|
host = f"http://{host}"
|
|
return host
|
|
|
|
@field_validator("azure_openai_base_url", mode="before")
|
|
@classmethod
|
|
def _normalize_azure_openai_base_url(cls, value: object) -> str:
|
|
from core.llm.providers.azure_openai import normalize_azure_openai_base_url
|
|
|
|
return normalize_azure_openai_base_url(str(value or ""))
|
|
|
|
@field_validator("provider", mode="before")
|
|
@classmethod
|
|
def _normalize_provider(cls, value: object) -> str:
|
|
provider = str(value or "anthropic").strip().lower() or "anthropic"
|
|
valid_providers = SUPPORTED_PROVIDER_VALUES
|
|
if provider in valid_providers:
|
|
return provider
|
|
suggestion = get_close_matches(provider, valid_providers, n=1)
|
|
if suggestion:
|
|
raise ValueError(
|
|
f"Unsupported LLM provider '{provider}'. Did you mean '{suggestion[0]}'?"
|
|
)
|
|
raise ValueError(
|
|
f"Unsupported LLM provider '{provider}'. Expected one of: {', '.join(valid_providers)}."
|
|
)
|
|
|
|
@model_validator(mode="after")
|
|
def _require_api_key_for_selected_provider(self) -> "LLMSettings":
|
|
if self.provider == "azure-openai" and not self.azure_openai_base_url:
|
|
raise ValueError(
|
|
"LLM provider 'azure-openai' requires AZURE_OPENAI_BASE_URL to be set."
|
|
)
|
|
return self
|
|
|
|
@classmethod
|
|
def from_env(cls) -> "LLMSettings":
|
|
"""Build validated LLM settings from environment variables."""
|
|
bootstrap_opensre_env(override=False)
|
|
return cls.model_validate(_llm_settings_env_payload(get_configured_llm_provider()))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LLMResolution:
|
|
"""Outcome of resolving LLM settings for the configured provider."""
|
|
|
|
settings: LLMSettings
|
|
configured_provider: str
|
|
resolved_provider: str
|
|
attempted_providers: tuple[str, ...]
|
|
missing_key_env: str | None
|
|
|
|
@property
|
|
def fell_back(self) -> bool:
|
|
"""True when the active provider differs from the configured one."""
|
|
return self.resolved_provider != self.configured_provider
|
|
|
|
def summary(self) -> str:
|
|
"""One-line, user-facing description of the active provider decision."""
|
|
return f"Using configured LLM provider '{self.resolved_provider}'."
|
|
|
|
|
|
def resolve_llm_settings_verbose(
|
|
fallback_providers: Sequence[str] = (),
|
|
) -> LLMResolution:
|
|
"""Resolve LLM settings without implicit provider fallback."""
|
|
bootstrap_opensre_env(override=False)
|
|
_ = fallback_providers
|
|
configured_provider = get_configured_llm_provider()
|
|
settings = LLMSettings.model_validate(_llm_settings_env_payload(configured_provider))
|
|
return LLMResolution(
|
|
settings=settings,
|
|
configured_provider=configured_provider,
|
|
resolved_provider=settings.provider,
|
|
attempted_providers=(configured_provider,),
|
|
missing_key_env=None,
|
|
)
|
|
|
|
|
|
def resolve_llm_settings(
|
|
fallback_providers: Sequence[str] = (),
|
|
) -> LLMSettings:
|
|
"""Resolve LLM settings for the configured provider only."""
|
|
return resolve_llm_settings_verbose(fallback_providers).settings
|
|
|
|
|
|
def describe_llm_resolution(
|
|
fallback_providers: Sequence[str] = (),
|
|
) -> str:
|
|
"""Return a human-readable LLM provider resolution report for diagnostics.
|
|
|
|
Safe to call even when no provider has usable credentials: instead of
|
|
raising it reports the missing-credentials condition. Intended for
|
|
``/status``, doctor commands, and CI diagnostics so operators no longer need
|
|
ad-hoc inline probes to see which provider is actually in use.
|
|
"""
|
|
try:
|
|
resolution = resolve_llm_settings_verbose(fallback_providers)
|
|
except ValidationError as exc:
|
|
configured = get_configured_llm_provider()
|
|
env_var = get_llm_provider_api_key_env(configured)
|
|
detail = exc.errors()[0].get("msg", str(exc)) if exc.errors() else str(exc)
|
|
lines = [
|
|
f"configured provider : {configured}",
|
|
"resolved provider : <none — no usable provider credentials>",
|
|
]
|
|
if env_var:
|
|
lines.append(f"required key : {env_var}")
|
|
lines.append(f"detail : {detail}")
|
|
return "\n".join(lines)
|
|
|
|
lines = [
|
|
f"configured provider : {resolution.configured_provider}",
|
|
f"resolved provider : {resolution.resolved_provider}",
|
|
f"auth method : {get_configured_llm_auth_method(resolution.resolved_provider)}",
|
|
"fell back : no",
|
|
f"providers attempted : {', '.join(resolution.attempted_providers)}",
|
|
]
|
|
auth_provider = effective_llm_provider(
|
|
resolution.resolved_provider,
|
|
get_configured_llm_auth_method(resolution.resolved_provider),
|
|
)
|
|
auth_status = credential_status(auth_provider)
|
|
lines.append(f"credential status : {auth_status.source} ({auth_status.detail})")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def llm_provider_error_context(
|
|
fallback_providers: Sequence[str] = (),
|
|
) -> str:
|
|
"""Return a short bracketed provider context for prefixing error messages.
|
|
|
|
Never raises — diagnostics must not mask the original error. Returns an
|
|
empty string when resolution itself fails so callers can fall back to the
|
|
raw provider error untouched.
|
|
"""
|
|
try:
|
|
resolution = resolve_llm_settings_verbose(fallback_providers)
|
|
except Exception:
|
|
return ""
|
|
return f"[LLM provider: {resolution.resolved_provider}]"
|
|
|
|
|
|
def has_credentials_for_active_llm_provider() -> bool:
|
|
"""Return prompt-safe auth availability for the configured LLM provider."""
|
|
settings = resolve_llm_settings()
|
|
auth_status = credential_status(
|
|
effective_llm_provider(settings.provider, os.getenv(LLM_AUTH_METHOD_ENV))
|
|
)
|
|
return auth_status.configured and not auth_status.stale
|
|
|
|
|
|
# LLM Provider Configs
|
|
ANTHROPIC_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=ANTHROPIC_REASONING_MODEL,
|
|
classification_model=ANTHROPIC_CLASSIFICATION_MODEL,
|
|
toolcall_model=ANTHROPIC_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
OPENAI_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=OPENAI_REASONING_MODEL,
|
|
classification_model=OPENAI_CLASSIFICATION_MODEL,
|
|
toolcall_model=OPENAI_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
OPENROUTER_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=OPENROUTER_REASONING_MODEL,
|
|
classification_model=OPENROUTER_CLASSIFICATION_MODEL,
|
|
toolcall_model=OPENROUTER_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
DEEPSEEK_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=DEEPSEEK_REASONING_MODEL,
|
|
classification_model=DEEPSEEK_CLASSIFICATION_MODEL,
|
|
toolcall_model=DEEPSEEK_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
GROQ_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=GROQ_REASONING_MODEL,
|
|
classification_model=GROQ_CLASSIFICATION_MODEL,
|
|
toolcall_model=GROQ_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
AZURE_OPENAI_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=AZURE_OPENAI_REASONING_MODEL,
|
|
classification_model=AZURE_OPENAI_CLASSIFICATION_MODEL,
|
|
toolcall_model=AZURE_OPENAI_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
GEMINI_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=GEMINI_REASONING_MODEL,
|
|
classification_model=GEMINI_CLASSIFICATION_MODEL,
|
|
toolcall_model=GEMINI_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
NVIDIA_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=NVIDIA_REASONING_MODEL,
|
|
classification_model=NVIDIA_CLASSIFICATION_MODEL,
|
|
toolcall_model=NVIDIA_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
MINIMAX_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=MINIMAX_REASONING_MODEL,
|
|
classification_model=MINIMAX_CLASSIFICATION_MODEL,
|
|
toolcall_model=MINIMAX_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
BEDROCK_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=BEDROCK_REASONING_MODEL,
|
|
classification_model=BEDROCK_CLASSIFICATION_MODEL,
|
|
toolcall_model=BEDROCK_TOOLCALL_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
OLLAMA_LLM_CONFIG = LLMModelConfig(
|
|
reasoning_model=DEFAULT_OLLAMA_MODEL,
|
|
classification_model=DEFAULT_OLLAMA_MODEL,
|
|
toolcall_model=DEFAULT_OLLAMA_MODEL,
|
|
max_tokens=DEFAULT_MAX_TOKENS,
|
|
)
|
|
|
|
# Tracer API Configuration
|
|
TRACER_BASE_URL_DEV = "https://staging.tracer.cloud"
|
|
TRACER_BASE_URL_PROD = "https://app.tracer.cloud"
|
|
SLACK_CHANNEL = "tracer-rca-report-alerts"
|
|
|
|
|
|
def get_tracer_base_url() -> str:
|
|
"""Get Tracer base URL for current environment."""
|
|
return (
|
|
TRACER_BASE_URL_PROD if get_environment() == Environment.PRODUCTION else TRACER_BASE_URL_DEV
|
|
)
|