LLM API
Purpose
engine.llm: the model each agent runs on, and replacing them at runtime.
Responsibilities
- List each agent and the provider and model it runs on (
list,get). - Report the prices every call is costed with (
prices). - Replace the engine's whole
llmsection at runtime (replace).
Key Modules
packages/nl2sql/src/nl2sql/api/llm_api.py(LLMAPI, internal)packages/nl2sql/src/nl2sql/llm/registry.pypackages/nl2sql/src/nl2sql/llm/llm_set.pypackages/nl2sql/src/nl2sql/llm/client_cache.pypackages/nl2sql/src/nl2sql/llm/models.pypackages/nl2sql/src/nl2sql/llm/prices.py(the dated price table,price_for)packages/nl2sql/src/nl2sql/config/models.py(LLM,AgentLLM: thellmsection)
Public Surface
AgentConfig
One agent's resolved settings, as LLMRegistry builds a client from them.
agents_from_config(llm) (llm/registry.py) makes one per agent from the
config's llm section (LLM configuration).
Source:
packages/nl2sql/src/nl2sql/llm/models.py
Fields:
| name | type | required | meaning |
| --- | --- | --- | --- |
| provider | str | yes | Provider name: openai, anthropic, openai_compatible, its presets openrouter and ollama, or one a plugin registers. |
| model | str | yes | Model identifier. |
| temperature | Optional[float] | no | Sampling temperature (default 0.0). None (null in YAML) sends no temperature parameter, for models that accept only their default. |
| api_key | Optional[SecretStr] | no | API key or secret reference. Masked in repr, str and JSON dumps. None reads the provider's standard variable. |
| base_url | Optional[str] | no | Endpoint override; defaults to the provider preset (https://openrouter.ai/api/v1 for openrouter, http://localhost:11434/v1 for ollama). Required for openai_compatible. |
| structured_output | Optional[str] | no | tool_calling, json_schema or json_mode. None uses the provider's default. |
| parse_retries | int | no | Retries of a structured answer that does not parse (default 1). |
| max_tokens | Optional[int] | no | None uses the provider's default (16,000 for anthropic, unset for the others). |
| timeout_s | Optional[float] | no | Per request. None uses the client library's. |
| max_retries | Optional[int] | no | Network and rate-limit retries, done by the client. None uses the client library's. |
| headers | Dict[str, SecretStr] | no | Sent with every request. A value may be a ${provider:key} reference, resolved when the client is built. Values are masked in repr, str and JSON dumps, and list() shows only the header names. |
| name | str | no | Agent name (default default). |
LLMFileConfig
A set of agents an eval in mode full compares, one config at a time:
LLMFileConfig.from_llm(llm) turns an llm section into it.
Source:
packages/nl2sql/src/nl2sql/llm/models.py
Fields:
| name | type | required | meaning |
| --- | --- | --- | --- |
| default | AgentConfig | yes | The top level of the llm section. |
| agents | Dict[str, AgentConfig] | no | The agents that set their own fields. |
llm.list
Signature:
list() -> dict
Returns each agent by its name in the llm section, default first: its
provider, model, temperature and inherited (True when the agent sets
nothing of its own and runs on default). An agent given a model object shows
its class as model and no provider. Never a key.
engine.llm.list()["ast_planner"]
# {'provider': 'openai', 'model': 'gpt-5.5', 'temperature': 0.0, 'inherited': False}
llm.get
Signature:
get(agent: str) -> dict
One agent's entry from list(). Raises ValueError for a name that is not an
agent, listing the agents.
llm.providers
Signature:
providers() -> list[dict]
Each LLM provider this engine can use: the built-ins whose client library is
installed, and any a plugin or Registry.add_llm registered. Each has name,
label, key_variable (the environment variable it reads its key from;
None for one that needs no key, such as ollama) and key_present (whether
that variable is set). Never the key.
engine.llm.providers()[0]
# {'name': 'anthropic', 'label': 'Anthropic', 'key_variable': 'ANTHROPIC_API_KEY', 'key_present': False}
llm.prices
Signature:
prices() -> dict
The prices every model call is costed with: the engine's dated table with
llm.prices applied (Prices).
checked_on is the day the table was checked against the providers' pages;
models maps each model name to its input, cached_input, cache_write
and output rates (USD per million tokens; a config row's cache rates may be
None, priced as the Prices section says), verified, source (table or
config) and citation (where a table row's numbers came from; None for a
config row). A model not listed is not priced: its calls' cost is null.
engine.llm.prices()["models"]["claude-opus-5"]
# {'input': 5.0, 'cached_input': 0.5, 'cache_write': 6.25, 'output': 25.0, 'verified': True,
# 'source': 'table', 'citation': 'Anthropic model table in the claude-api skill (cached 2026-06-24); ...'}
llm.replace
Source:
packages/nl2sql/src/nl2sql/api/llm_api.py (Runtime.replace_llm in
packages/nl2sql/src/nl2sql/runtime.py)
Signature:
replace(llm: LLM | Mapping, *, by: Optional[UserContext] = None, via: str = "python") -> LLMChange
Parameters:
| name | type | required | meaning |
| --- | --- | --- | --- |
| llm | LLM or a mapping | yes | The whole llm section: an LLM, or the section as written in nl2sql.yaml (a mapping, such as a request body). A mapping is checked exactly as the file is: strict keys with "did you mean", the model's validators, references kept in secret fields. A ${...} reference outside a secret field is an error rather than read, since the data did not come from the operator's file. |
| by | Optional[UserContext] | no | Who is making the change; its user_id and roles are logged. |
| via | str | no | The entry point making the change (python, api, playground). |
Returns:
LLMChange (exported from nl2sql): by (via, and user_id and roles
when by was given) and changes, each agent whose effective provider or
model changed as {"old": "provider/model", "new": "provider/model"}. It holds
no key.
Raises:
- ConfigError for a section that does not validate, a provider that is not
registered or not installed, an agent whose API key does not resolve, or,
with settings.pipeline.max_cost_usd set, a model with no price.
Nothing changes. nl2sql-api answers it as HTTP 422 with the same text
(PUT /api/v1/settings/llm).
Side Effects:
- Builds the new clients (no model call), then swaps the engine's LLMSet
and engine.config.llm together under the engine's lock. Runs already in
flight finish on the old set.
- One INFO line on the nl2sql.llm logger and one llm.changed audit record.
Nothing is written to disk.
To use the llm section of a file: engine.llm.replace(load_config(path).llm).
See LLM configuration → Replacing the LLM at runtime.
Internals: AgentConfig and LLMFileConfig
AgentConfig (llm/models.py) is one agent's resolved settings, as
LLMRegistry builds a client from them; agents_from_config(llm)
(llm/registry.py) makes one per agent from the llm section. Its fields are
provider, model, temperature (None sends no temperature parameter),
api_key (masked in repr, str and JSON dumps; None reads the provider's
standard variable), base_url, structured_output, parse_retries, max_tokens, timeout_s,
max_retries, headers (values masked like the key) and name. LLMFileConfig is a set of agents
an eval in mode full compares, one config at a time (LLMFileConfig.from_llm(llm)).
Neither is part of the public API.
Behavioral Contracts
- The built-in providers are
openai,anthropicandopenai_compatible, andopenrouterandollama, which are named presets ofopenai_compatiblewith no code path of their own. They are rows ofPROVIDER_PRESETSinllm/registry.py, which stays the one table of key variables, andLLM_PROVIDERSinllm/providers.pymakes oneLLMProviderper row. Any other provider is a plugin: a class implementingLLMProvider, interface version 2, registered withregistry.add_llmor thenl2sql.llmsentry point group. An unregistered name raisesValueError: Unsupported LLM provider, naming the valid ones. LLMProviderversion 2 hasinterface_version,label,api_key_env,requires,install_hint,structured_output(the method an agent that sets none is asked with) andcreate(agent: ResolvedAgent).ResolvedAgentcarries the agent'sname,model,temperature, resolvedapi_key,base_url,structured_output(the agent's, else the provider's),max_tokens,timeout_s,max_retriesand resolvedheaders; neither the key nor a header value is in itsrepr. A provider of another version is rejected when it is loaded.- Each preset names a wire type (
ProviderPreset.wire), and the client is built by that wire's adapter inllm/wires/(WIRES[preset.wire].build_client). TheWireprotocol (llm/wires/base.py) isbuild_client(model, temperature, **kwargs),structured_output_method,mark_cache(payload)andread_usage(response).wires.structured(llm, schema)is how every node asks for structured output, with the agent'sstructured_outputmethod, which the registry records on the client it builds (tool_callingis LangChain'sfunction_calling); forjson_modeit adds the schema to the prompt, since JSON mode alone does not carry it. An answer that does not parse is sent back with its validation errors, up to the agent'sparse_retriestimes (recorded on the client too), and thenwires.StructuredOutputError(agent, schema, attempts, errors)is raised, whichfailures.provider_error()reports asMODEL_OUTPUT_INVALID.wires.streaming_structured(llm, schema)is the same with the first answer streamed; a retry is not streamed. A model object given in the config uses its wire'sstructured_output_methodand one retry.wires.wire_of(llm)picks the adapter from the client. See LLM configuration → Providers and wire types. openai,openai_compatible,openrouterandollamaare on theopenaiwire, served byConfiguredChatOpenAI(llm/wires/openai.py).openaiuses the client's own default endpoint.openai_compatiblehas no endpoint of its own: without abase_urlits agent raises aValueErrorasking for one.openrouterdefaults tohttps://openrouter.ai/api/v1andollamatohttp://localhost:11434/v1; a config-suppliedbase_urloverrides either.anthropicis on theanthropicwire, served byCachingChatAnthropic(llm/wires/anthropic_client.py), alangchain-anthropicChatAnthropicwhose requests go through the adapter'smark_cache(acache_control: {"type": "ephemeral"}breakpoint on the last system block), withmax_tokens=16000unless the agent setsmax_tokens, and whose tool calls go through the adapter'srepair_tool_calls(arguments Claude nested under a single key the schema does not declare --input,query, ... -- are unwrapped). It needs thenl2sql-engine[anthropic]extra; without itget_llm()raises aValueErrornaming the extra. See LLM configuration → Anthropic.openai,anthropicandopenrouterrequire an API key (OPENAI_API_KEY,ANTHROPIC_API_KEY,OPENROUTER_API_KEY).openai_compatibleandollamado not: without anapi_keytheir preset supplies the placeholder keyChatOpenAIinsists on.openai_compatiblenever readsOPENAI_API_KEY.max_tokens,timeout_s,max_retriesandheadersreach the client when set (ChatOpenAI's andChatAnthropic'smax_tokens,timeout,max_retriesanddefault_headers); unset, the client's own default holds.- The engine's LLM is one immutable
LLMSet(each agent's settings and client), built when the engine is created and on everyreplace, without a model call. A run reads it once, when it starts (LLMRegistry.begin_run), so a run never mixes two sets. - Clients come from the engine's
LLMClientCache, an LRU of 16 keyed on the agent's resolved settings with the API key and each header value only as a SHA-256 digest. Agents with equal settings share one client, and swapping back reuses cached clients. A client that falls out of the cache is closed (itsclose(), when it has one) once no set in use holds it;engine.close()closes them all. - A provider that needs a key and cannot resolve one does not stop the engine
being built:
check()reports it (missing_keys()), andget_llm()for that agent raises aValueErrornaming the agent, the provider and the environment variable to set - not the rawopenai.OpenAIError: Missing credentials. - Ollama is supported at the transport level. The pipeline calls
with_structured_output(including the recursivePlanModelin the AST planner), so results depend on the chosen local model's structured-output capability. LLMRegistry.get_llm()falls back todefaultif the name is missing, and raisesValueErrornaming the missing agent when nodefaultis registered either.- Embeddings are not routed through
LLMRegistry: each engine builds one embedder from itsembeddingssection, through the registry's embeddings providers (openai, which needs an OpenAI key even when chat runs through OpenRouter, orlocal, the key-free ONNX model bundled with chromadb), or uses theEmbeddings(instance=...)it is given. Two engines never share one. - Determinism: every OpenAI-protocol client is built with
seed=42(Anthropic has no seed parameter), and every client with the agent's configuredtemperature, or none when it isnull(see LLM configuration → Temperature). - A provider's HTTP 400 rejecting
temperatureis re-raised as aValueErrornaming the model and agent and telling you to settemperature: null. LLMRegistry.register_llms(internal) registers each agent under its key in the mapping, whatever itsnamefield says.