File size: 8,603 Bytes
4e1037f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | from functools import lru_cache
from typing import Literal
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
ProviderName = Literal[
"mistral",
"github_models",
"groq",
"ollama",
"openrouter",
"perplexity",
"gracekelly",
"zen",
"grok_cli",
"claude_cli",
"local_vllm",
]
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="NL_SQL_",
extra="ignore",
case_sensitive=False,
populate_by_name=True,
)
log_level: str = "INFO"
default_provider: ProviderName = "mistral"
frontier_provider: ProviderName = "groq" # GitHub Models needs fine-grained PAT
local_provider: ProviderName = "ollama"
mistral_gen_model: str = "codestral-latest"
mistral_nl_model: str = "mistral-large-latest"
mistral_embed_model: str = "mistral-embed"
mistral_base_url: str = "https://api.mistral.ai/v1"
github_models_model: str = "openai/gpt-4o-mini"
github_models_base_url: str = "https://models.github.ai/inference"
groq_model: str = "llama-3.3-70b-versatile"
groq_base_url: str = "https://api.groq.com/openai/v1"
ollama_gen_model: str = "qwen2.5-coder:7b-instruct"
ollama_base_url: str = "http://localhost:11434/v1"
ollama_timeout_seconds: float = 180.0
# OpenRouter β heterogeneous-CSC slot. Default = deepseek-v4-flash:free
# (DeepSeek family, β Mistral β needed so self-consistency votes don't
# collapse into one model's blind spots, as happened in config F + CSC
# merge-revision saturation on homogeneous codestral). Earlier picks
# rejected during 2026-05-20 probe:
# - z-ai/glm-4.5-air:free β reasoning model, 2186 reasoning_tokens
# consumed the whole budget, content=empty (smoke5 β 0% EA).
# - qwen/qwen3-coder:free β Venice provider 429-loop (free quota).
# deepseek-v4-flash:free returned valid JSON+SQL on probe (LIMIT/OFFSET
# correct for 7th-row case). Other live free models cycle; check
# `D:\TXT\Free API Keys.txt` / smoke before switching.
# `deepseek/deepseek-v4-flash:free` used to live here and no longer exists β
# OpenRouter answers 404 and points at the paid slug, so this provider had
# been quietly dead. qwen3-coder is free, code-specialised and long-context;
# note the free tier is rate-limited upstream (429) and needs retry patience.
openrouter_model: str = "qwen/qwen3-coder:free"
openrouter_base_url: str = "https://openrouter.ai/api/v1"
# OpenCode Zen β free generator slot (see llm/providers/zen.py). Only the
# `*-free` catalogue is reachable: the workspace has no payment method, so
# paid ids answer CreditsError. Free ids as of 2026-07-14, all of which
# emitted valid SQL on probe: deepseek-v4-flash-free, north-mini-code-free,
# nemotron-3-ultra-free, mimo-v2.5-free, hy3-free, big-pickle.
zen_model: str = "deepseek-v4-flash-free"
zen_base_url: str = "https://opencode.ai/zen/v1"
zen_timeout_seconds: float = 180.0
# Grok Build CLI β subscription path (weekly quota, like GraceKelly), no key.
# `grok models` advertises grok-4.20-0309-reasoning but the backend rejects it;
# the tier serves grok-composer-2.5-fast. See llm/providers/grok_cli.py.
grok_cli_path: str = "grok"
grok_cli_model: str = "grok-composer-2.5-fast"
grok_cli_timeout_seconds: float = 600.0
grok_cli_max_turns: int = 4
"""Must be > 1. Grok spends its first turn announcing what it is about to do;
`--max-turns 1` returns that preamble instead of the SQL (47.0% / 31% invalid
on the first n=200 run). See llm/providers/grok_cli.py."""
grok_cli_effort: str | None = None
"""`--reasoning-effort`. None leaves the CLI on its own default."""
# Claude Code CLI β subscription path (plan limits, no key). `--system-prompt`
# replaces the harness prompt, so the model answers as a completion, not an
# agent. See llm/providers/claude_cli.py.
claude_cli_path: str = "claude"
claude_cli_model: str = "claude-sonnet-5"
claude_cli_timeout_seconds: float = 600.0
claude_cli_max_turns: int = 4
"""Must be > 1: a denied tool call eats a turn and the CLI then answers
`error_max_turns` instead of the SQL."""
claude_cli_effort: str | None = None
"""`--effort` (low|medium|high|xhigh|max). None leaves the CLI on its own
default. The spawned process does NOT inherit the effort of the session that
launched it, so this is the only way to run a max-effort ablation."""
# Local vLLM β rented-GPU student-model slot (S2, plan_autotune.md; see
# llm/providers/local_vllm.py). No fixed host like Ollama's :11434: the
# box only exists for the duration of a GPU rental, so `base_url` must be
# set in `.env` each session. `NL_SQL_LOCAL_LLM_API_KEY` defaults to the
# literal "dummy" β vLLM's OpenAI-compatible server does not check a key
# unless started with `--api-key`, but the openai SDK requires a
# non-empty string.
local_llm_base_url: str = "http://localhost:8000/v1"
local_llm_model: str = "Qwen/Qwen2.5-Coder-7B-Instruct"
local_llm_api_key: str = "dummy"
local_llm_timeout_seconds: float = 180.0
# Perplexity browser path via local GraceKelly (D:\GraceKelly). Free
# because it rides the user's Perplexity Pro subscription via Playwright.
# `claude-sonnet-4-6` here is the Perplexity menu label, not the
# Anthropic API model id β GraceKelly resolves it to the browser path.
perplexity_browser_model: str = "claude-sonnet-4-6"
perplexity_base_url: str = "http://127.0.0.1:8011"
# GraceKelly orchestrate path β the AgentFlow branch runs NL->SQL on
# Claude Sonnet 5 via GraceKelly (AgentFlow ADR 0008), not Mistral. The
# portfolio default_provider stays `mistral`; this slot is selected
# explicitly via build_provider("gracekelly"). `claude-sonnet-5` resolves
# through GraceKelly's live catalog to "Claude Sonnet 5.0".
gracekelly_model: str = "claude-sonnet-5"
gracekelly_base_url: str = "http://127.0.0.1:8011"
gracekelly_timeout_seconds: float = 900.0
"""Browser path, so this is a human-scale wait, not an API round-trip.
Measured on the NL_SQL generation prompt (schema + few-shot + rules, ~6k
tokens) against Perplexity: 150-436s per call. The former 180s default
timed out mid-benchmark and surfaced as `pipeline_exception`, which scores
as a miss and silently understates the model. 900s leaves headroom."""
mistral_api_key: str = Field(default="", validation_alias="MISTRAL_API_KEY")
github_token: str = Field(default="", validation_alias="GITHUB_TOKEN")
groq_api_key: str = Field(default="", validation_alias="GROQ_API_KEY")
openrouter_api_key: str = Field(default="", validation_alias="OPENROUTER_API_KEY")
zen_api_key: str = Field(default="", validation_alias="OPENCODE_API_KEY")
"""One or more OpenCode Zen keys, comma-separated. Free tiers meter per key,
so extra keys are extra headroom: the provider rotates through them on 429."""
# Optional API auth for the FastAPI surface. When empty, /ask and /databases
# are open β but the rate limiter still applies (keyed by client IP), so a
# keyless public deploy is not unthrottled. Read here (not os.environ) so a
# value in .env is honoured, per the api/main module docstring.
api_key: str = Field(default="", validation_alias="NL_SQL_API_KEY")
# Optional Postgres target. When `pg_dsn` is set, the registry registers a
# Postgres-backed database under `pg_db_id`. Point it at the read-only role
# (nl_sql_ro) for defence in depth β the engine also forces read-only
# transactions on top (see db/connection.py). Empty = SQLite-only (default).
pg_dsn: str = Field(default="", validation_alias="NL_SQL_PG_DSN")
pg_db_id: str = "pg_codebase_community"
pg_description: str = "StackExchange-derived BIRD codebase_community, loaded into Postgres 16."
# diskcache for LLM generate/embed responses (per docs/02_architecture_v2.md Β§6.5).
# Two subdirs ("gen", "embed") are created under this root by `nl_sql.llm.cache`.
llm_cache_dir: str = ".cache/llm"
llm_cache_size_limit_gb: int = 4
@lru_cache(maxsize=1)
def get_settings() -> Settings:
return Settings()
|