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()