5. Agent And LLM Configuration#
The agent layer can run in two modes:
deterministic mode, where agents use pycsamt algorithms, rules, and validators without contacting an external LLM;
LLM-assisted mode, where selected agents add natural-language parsing, interpretation, reporting, planning, and explanation on top of the same pycsamt computations.
The important design point is that LLM configuration is centralized. You
configure the active provider once through
pycsamt.agents.AGENT_CONFIG, and every agent created
afterwards inherits the provider, model, API key, pricing table, and
optional session budget – inherits at construction time specifically,
which turns out to be the one subtlety worth internalizing before anything
else on this page: an already-built agent never notices a later
AGENT_CONFIG.switch(...), only agents built after it do.
This page should be read before using the agent catalogue. It explains what is configured, how pycsamt finds credentials, how to keep workflows reproducible, and how to avoid surprising costs.
5.1. Quick decision guide#
Use the table below to choose the right setup for the situation.
Situation |
Recommended setup |
Why |
|---|---|---|
CI, tests, tutorials, or offline work |
Do not configure an API key. |
Agents still run their deterministic pycsamt logic and skip LLM calls. |
Personal workstation |
Store the key in an environment variable and call
|
Secrets stay outside notebooks, scripts, and documentation. |
Script with an explicit runtime secret |
Use |
The script is explicit about the provider and model for the session. |
One agent needs a different model |
Pass |
Only that agent changes; the global configuration remains intact. |
A short experiment needs another provider |
Use
|
The original session configuration is restored automatically. |
You need a hard cost limit |
Use
|
pycsamt raises
|
5.2. Configuration objects#
The public configuration API is exposed from pycsamt.agents:
>>> from pycsamt.agents import (
... AGENT_CONFIG,
... AgentConfig,
... BudgetExceededError,
... configure_agents,
... reset_agents,
... )
AGENT_CONFIG is a process-local singleton, similar in spirit to the
plotting, section, style, and station-rendering API objects in pycsamt. It is
not written to disk and is not shared between Python processes – and it really
is the same object everywhere it is imported from, including the lower-level
module it is defined in:
>>> from pycsamt.agents import AGENT_CONFIG as via_agents
>>> from pycsamt.api.agents import AGENT_CONFIG as via_api
>>> via_agents is via_api
True
Configure it at the beginning of a notebook, script, CLI command, or application session.
configure_agents(...) and reset_agents(...) are convenience wrappers
around AGENT_CONFIG.configure(...) and AGENT_CONFIG.reset(...).
5.3. What gets configured#
Field |
Meaning |
Used by |
|---|---|---|
|
Active LLM provider – see the five supported families below. |
Every new agent unless it is explicitly overridden. |
|
Credential resolved from an explicit key or an environment variable. |
|
|
Model identifier for the active provider. |
All LLM calls made by the agent. |
|
Optional USD-per-million-token rates for models not in the built-in table, or for rates you want to override. |
Cost estimates and budget accounting. |
|
Optional hard session cap. |
Checked before every LLM call. |
|
Accumulated estimated LLM spend in the current Python process. |
Reports, dashboards, logs, and budget checks. |
The API key itself is never returned in full by
AGENT_CONFIG.info; the display
value is masked to the last four characters.
5.4. Supported providers and defaults#
pycsamt supports five LLM provider families. If you select a provider but do not pass a model, pycsamt uses the default listed here.
Provider |
Default model |
Environment variables checked |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DeepSeek and MiniMax are OpenAI-compatible REST APIs reached through the same
openai client with a provider-specific base_url, so no separate
client package is needed for either. For the other three, install the
matching client package only for the provider you intend to use:
1pip install anthropic
2pip install openai
3pip install google-generativeai
The imports are lazy. Importing pycsamt.agents does not require any
of these client libraries to be installed, and none is imported until an
agent actually calls query_llm() with a resolved key.
5.5. Recommended setup with environment variables#
For day-to-day use, keep secrets outside code and configure the active provider from the environment.
1export ANTHROPIC_API_KEY="sk-ant-demo1a2b"
Then activate the provider in Python:
>>> from pprint import pprint
>>> from pycsamt.agents import AGENT_CONFIG
>>> AGENT_CONFIG.switch("claude", model="claude-sonnet-4-6")
>>> pprint(AGENT_CONFIG.info())
{'api_key_masked': '…1a2b',
'budget_usd': None,
'custom_rate_models': {},
'has_key': True,
'key_source': 'env',
'model': 'claude-sonnet-4-6',
'provider': 'claude',
'remaining_usd': None,
'spent_usd': 0.0,
'stored_providers': []}
switch(...) is the right call when the key is already available through
the environment or was previously registered with set_key(...). It selects
the provider and optional model without embedding the key in the script. The
information dictionary above is safe to log or display: api_key_masked
shows only the last four characters, and key_source: 'env' confirms the
key came from ANTHROPIC_API_KEY rather than a value stored in
AGENT_CONFIG itself.
5.6. Explicit Python setup#
Use explicit setup when the key is supplied by a secret manager, CLI option,
or application settings layer. runtime_secret below stands in for
whatever your settings layer actually returns:
>>> from pycsamt.agents import configure_agents, AGENT_CONFIG
>>> runtime_secret = "sk-openai-demo9z9z"
>>> configure_agents(
... provider="openai",
... api_key=runtime_secret,
... model="gpt-4o-mini",
... )
>>> AGENT_CONFIG.info()["key_source"]
'explicit'
>>> AGENT_CONFIG.info()["stored_providers"]
['openai']
Unlike the environment-variable path above, key_source now reads
'explicit' – the key was handed to pycsamt directly rather than
discovered in the process environment – and stored_providers records
that a key is now held for "openai" specifically, which is what lets a
later switch() back to it skip
re-supplying the secret. The same operation can be written with the
singleton directly instead of the configure_agents convenience wrapper:
>>> AGENT_CONFIG.configure(
... provider="claude",
... api_key=runtime_secret,
... model="claude-sonnet-4-6",
... )
Unlike switch(...), configure(...) requires an api_key argument.
That makes it explicit when code is placing a secret into the in-memory
configuration object.
5.7. How agents resolve LLM settings#
Every agent inherits from BaseAgent. During
initialization, BaseAgent calls AGENT_CONFIG.resolve(...) to decide
which provider, key, and model the new agent should use.
The resolution rules are:
If the agent constructor receives an explicit
api_key, that agent uses the constructor’sllm_provider,api_key, andmodel. The global configuration is ignored for that agent.If the constructor does not receive an
api_keyand itsllm_provideris"claude"–BaseAgent’s default – the agent inherits the active global provider when one has been configured. This rule cannot tell an explicitllm_provider="claude"apart from simply not passingllm_providerat all, since both look identical by the timeresolve()sees them; see the callout in per-agent overrides for the consequence.The key for the effective provider is resolved as: stored key, then provider-specific environment variables, then
None.The model comes from the constructor if supplied. Otherwise, when the effective provider matches the active global provider, the global model is used. If no model is available, pycsamt uses the provider default.
If no key is found, the agent still runs but skips LLM calls. Returned
AgentResult.llm_interpretationvalues will beNoneand LLM cost remains zero.
This behavior keeps simple workflows concise:
>>> from pycsamt.agents import AGENT_CONFIG, DataQCAgent, PhaseAnalysisAgent
>>> AGENT_CONFIG.switch("openai", model="gpt-4o-mini")
>>> qc = DataQCAgent() # inherits OpenAI + gpt-4o-mini
>>> pt = PhaseAnalysisAgent() # same provider and model
>>> qc.llm_provider, qc.model
('openai', 'gpt-4o-mini')
>>> pt.llm_provider, pt.model
('openai', 'gpt-4o-mini')
5.8. Per-agent overrides#
Use a per-agent override when a specific task benefits from a different model
or provider. For example, you might keep the global configuration on a fast
model, but ask the report agent to use a stronger model for final prose. The
reliable way to do this is to pass an explicit api_key along with
llm_provider – rule 1 above then applies and the global configuration is
bypassed completely for that one agent:
>>> from pycsamt.agents import AGENT_CONFIG, DataQCAgent, ReportAgent
>>> AGENT_CONFIG.switch("openai", model="gpt-4o-mini")
>>> qc_agent = DataQCAgent()
>>> gemini_runtime_secret = "AIza-demo"
>>> report_agent = ReportAgent(
... llm_provider="gemini",
... api_key=gemini_runtime_secret,
... model="gemini-2.0-flash",
... )
>>> report_agent.llm_provider, report_agent.model
('gemini', 'gemini-2.0-flash')
Omitting api_key and passing only llm_provider and model looks
like it should do the same thing, and it does – for every provider except
"claude", because "claude" is also BaseAgent’s constructor
default. Rule 2 above cannot distinguish “the caller explicitly asked for
Claude” from “the caller did not pass llm_provider at all”, so an
explicit llm_provider="claude" with no key silently inherits whatever
provider is globally active instead:
>>> broken = ReportAgent(
... llm_provider="claude",
... model="claude-sonnet-4-6",
... )
>>> broken.llm_provider, broken.model
('openai', 'claude-sonnet-4-6')
broken now holds an OpenAI provider paired with a Claude model string –
not an agent waiting on a Claude key, as it would be reasonable to assume
from the constructor call alone. It would run past construction without
error and only fail once query_llm() actually sent "claude-sonnet-4-6"
to the OpenAI API. Avoid it the same way the working example above does:
pass an explicit api_key whenever the target provider is "claude" and
might differ from whatever is globally active, or wrap the construction in
AGENT_CONFIG.using (below)
instead of relying on constructor arguments alone.
5.9. Multi-provider sessions#
You can register keys for several providers and switch between them during the same Python session.
>>> from pycsamt.agents import AGENT_CONFIG, DataQCAgent
>>> claude_key, openai_key, gemini_key = (
... "sk-ant-demo1a2b", "sk-openai-demo9z9z", "AIza-demo",
... )
>>> AGENT_CONFIG.set_key("claude", claude_key)
>>> AGENT_CONFIG.set_key("openai", openai_key)
>>> AGENT_CONFIG.set_key("gemini", gemini_key)
>>> AGENT_CONFIG.switch("claude")
>>> agent_a = DataQCAgent() # constructed while Claude is active
>>> agent_a.llm_provider, agent_a.model
('claude', 'claude-sonnet-4-6')
>>> AGENT_CONFIG.switch("openai", model="gpt-4o-mini")
>>> agent_b = DataQCAgent() # constructed while OpenAI is active
>>> agent_b.llm_provider, agent_b.model
('openai', 'gpt-4o-mini')
>>> agent_a.llm_provider, agent_a.model # unchanged by the later switch
('claude', 'claude-sonnet-4-6')
Existing agent instances keep the provider, key, and model they resolved at
construction time – agent_a above never notices the later switch to
OpenAI, exactly the behavior flagged in the introduction. Switch before
constructing the agents that should use the new provider, not after.
5.10. Temporary overrides#
Use AGENT_CONFIG.using(...) when a block of code should temporarily use a
different provider or model. Unlike the per-agent llm_provider="claude"
pitfall above, using(...) changes what “the active global provider” is
for the duration of the block, so the ordinary construction-time inheritance
rules apply correctly inside it – no special-cased provider name to trip
over:
>>> from pycsamt.agents import AGENT_CONFIG, InterpretationAgent
>>> AGENT_CONFIG.switch("openai", model="gpt-4o-mini")
>>> with AGENT_CONFIG.using(provider="claude", model="claude-sonnet-4-6"):
... print("inside:", AGENT_CONFIG.provider, AGENT_CONFIG.model)
... interpretation = InterpretationAgent()
... print("agent: ", interpretation.llm_provider, interpretation.model)
inside: claude claude-sonnet-4-6
agent: claude claude-sonnet-4-6
>>> print("after: ", AGENT_CONFIG.provider, AGENT_CONFIG.model)
after: openai gpt-4o-mini
The OpenAI configuration is restored automatically on exit – the context
manager also restores custom rates and budget state if an exception occurs
inside the block. interpretation itself keeps the Claude provider it
resolved at construction time even after the block ends, following the same
construction-time-inheritance rule as everywhere else on this page; only
agents built inside the with block get the temporary override.
5.11. Running without an LLM#
LLM support is optional. If no key is configured, agents skip calls to external services and return deterministic pycsamt outputs.
>>> from pycsamt.agents import DataQCAgent, reset_agents
>>> reset_agents()
>>> result = DataQCAgent().execute({"path": "data/AMT/WILLY_DATA/L18PLT"})
>>> assert result.cost_estimate_usd == 0.0
>>> assert result.llm_interpretation is None
>>> result.status
'success'
Both assertions hold and the QC run still succeeds – reset_agents()
clears every stored key and the active provider, so this is the same
graceful degradation documented throughout Agent Catalogue and
Foundation And Survey Intake Agents, exercised here explicitly rather than as a side
effect of simply never having configured a key.
This mode is the recommended default for:
unit tests;
documentation examples;
reproducible batch processing;
secure environments where network calls are not allowed;
workflows where only numerical outputs are required.
Agents that are mainly computational, such as QC, tensor rotation, EDI export, pipeline execution, and inversion preparation, still provide useful results in deterministic mode. Agents that are mainly conversational or explanatory, such as context parsing, interpretation, and report writing, may produce simpler outputs or mark interpretation fields as unavailable.
5.12. Budget caps#
Budget caps protect long workflows from unexpected LLM spend. The cap is
session-local, and remaining_usd is a direct read of the two counters
underneath it:
clamped at zero rather than going negative once the cap is reached. The
check itself, spent_usd >= budget_usd, runs inside
BaseAgent.query_llm immediately before the network call, so a workflow
fails fast on the call that would exceed the cap rather than after paying
for it. Simulating “several prior calls already reached the cap” with
_add_spend – the same accumulator query_llm updates after every
real response – triggers the identical, real exception without needing a
network connection or a valid key to demonstrate it:
>>> from pycsamt.agents import AGENT_CONFIG, BudgetExceededError
>>> AGENT_CONFIG.switch("claude")
>>> AGENT_CONFIG.set_budget(usd=2.00)
>>> AGENT_CONFIG._add_spend(2.00) # stand-in for prior LLM calls
>>> try:
... AGENT_CONFIG._check_budget()
... except BudgetExceededError as exc:
... print(f"Budget ${exc.budget_usd:.2f} reached.")
... print(f"Already spent ${exc.spent_usd:.4f}.")
Budget $2.00 reached.
Already spent $2.0000.
A real workflow never calls _check_budget() or _add_spend()
directly – query_llm() does both automatically; they are shown here
only to make the failure reproducible without live credentials. Inspect the
remaining budget, and reset it, the same way at any point:
>>> AGENT_CONFIG.spent_usd
2.0
>>> AGENT_CONFIG.remaining_usd
0.0
>>> AGENT_CONFIG.reset_budget() # zero the counter, keep the $2.00 cap
>>> AGENT_CONFIG.spent_usd, AGENT_CONFIG.remaining_usd
(0.0, 2.0)
>>> AGENT_CONFIG.reset_budget(cap=True) # also remove the cap entirely
>>> print(AGENT_CONFIG.remaining_usd)
None
5.13. Pricing and rate overrides#
pycsamt estimates cost from provider rates expressed in USD per 1,000,000 tokens, using exactly the \(\text{cost}_{\text{USD}} = (n_{\text{in}} r_{\text{in}} + n_{\text{out}} r_{\text{out}}) / 10^{6}\) formula introduced in Foundation And Survey Intake Agents. Built-in rates are used when the model is known. If a model is new, or if your organization wants to use a different accounting rate, add an override:
>>> from pycsamt.agents import AGENT_CONFIG
>>> AGENT_CONFIG.set_rate(
... "openai",
... "gpt-5-turbo",
... input=5.00,
... output=20.00,
... )
>>> cost = AGENT_CONFIG.estimate_cost(
... "openai",
... "gpt-5-turbo",
... input_tokens=12_000,
... output_tokens=2_000,
... )
>>> print(f"Estimated cost: ${cost:.4f}")
Estimated cost: $0.1000
\((12{,}000 \cdot 5.00 + 2{,}000 \cdot 20.00) / 10^{6} = 0.10\) – the
override took effect immediately, with no need to restart a session or
reconstruct already-built agents, because estimate_cost and
query_llm both read the rate table live rather than caching it. List
the effective rate table for a provider to see overrides merged with the
built-in models:
>>> from pprint import pprint
>>> pprint(AGENT_CONFIG.list_rates("claude"))
{'claude-3-5-sonnet-20241022': {'input': 3.0, 'output': 15.0},
'claude-3-haiku-20240307': {'input': 0.25, 'output': 1.25},
'claude-3-opus-20240229': {'input': 15.0, 'output': 75.0},
'claude-haiku-4-5': {'input': 0.8, 'output': 4.0},
'claude-haiku-4-5-20251001': {'input': 0.8, 'output': 4.0},
'claude-opus-4-6': {'input': 15.0, 'output': 75.0},
'claude-opus-4-7': {'input': 15.0, 'output': 75.0},
'claude-opus-4-8': {'input': 15.0, 'output': 75.0},
'claude-sonnet-4-6': {'input': 3.0, 'output': 15.0}}
Rate resolution order is:
exact custom override;
exact built-in model rate;
prefix match in custom rates or built-in rates;
provider-level fallback rate.
Cost estimates are approximate. They are intended for workflow accounting and budget control, not invoice reconciliation.
5.14. Inspecting active configuration#
Use AGENT_CONFIG.info() when debugging notebooks, application sessions, or
agent workflows – it is the same call, and the same dictionary shape, shown
running for real in the “Recommended setup” and “Explicit Python setup”
sections above. Important fields are:
providerThe active provider, or
Noneif no provider has been selected.modelThe active model, including provider defaults.
has_keyWhether the active provider has a resolvable key.
key_source"explicit"for keys stored inAGENT_CONFIG,"env"for environment variables, or"none".stored_providersProviders with explicit keys stored in the current process.
custom_rate_modelsModels whose rates were overridden for this session.
budget_usd,spent_usd,remaining_usdCurrent budget state.
5.15. Resetting configuration#
Reset everything, including stored keys, or keep stored keys for later switching and clear only the active selection, custom rates, and budget:
>>> from pycsamt.agents import AGENT_CONFIG, reset_agents
>>> AGENT_CONFIG.set_key("claude", "sk-ant-demo")
>>> AGENT_CONFIG.switch("claude")
>>> AGENT_CONFIG.set_budget(usd=1.0)
>>> reset_agents(keys=False)
>>> AGENT_CONFIG.info()["stored_providers"]
['claude']
>>> AGENT_CONFIG.info()["provider"], AGENT_CONFIG.info()["budget_usd"]
(None, None)
>>> reset_agents()
>>> AGENT_CONFIG.info()["stored_providers"]
[]
reset_agents(...) does not clear environment variables. If an environment
variable is still present, a later AGENT_CONFIG.switch(provider) can resolve
that key again even after a full reset_agents().
5.16. Practical recipes#
5.16.1. Offline QC recipe#
Use this pattern for tests, documentation, or secure offline processing.
>>> from pycsamt.agents import DataQCAgent, reset_agents
>>> reset_agents()
>>> qc = DataQCAgent()
>>> result = qc.execute({"path": "data/AMT/WILLY_DATA/L18PLT"})
>>> print(result.status)
success
>>> print(result.cost_estimate_usd)
0.0
5.16.2. Local analyst recipe#
Use environment variables, choose a provider, set a small budget, then create agents normally.
>>> from pycsamt.agents import AGENT_CONFIG, DataQCAgent, ReportAgent
>>> AGENT_CONFIG.switch("claude", model="claude-sonnet-4-6")
>>> AGENT_CONFIG.set_budget(usd=1.50)
>>> qc = DataQCAgent()
>>> report = ReportAgent()
>>> qc.llm_provider, report.llm_provider
('claude', 'claude')
>>> AGENT_CONFIG.remaining_usd
1.5
5.16.3. Mixed-model recipe#
Use a fast global model for routine steps and a stronger model for the final
interpretation – using(...) rather than a bare llm_provider="claude"
constructor argument, per the per-agent overrides caveat above:
>>> from pycsamt.agents import AGENT_CONFIG, DataQCAgent, InterpretationAgent
>>> AGENT_CONFIG.switch("openai", model="gpt-4o-mini")
>>> qc_agent = DataQCAgent()
>>> with AGENT_CONFIG.using(provider="claude", model="claude-sonnet-4-6"):
... interpretation_agent = InterpretationAgent()
... print(interpretation_agent.llm_provider, interpretation_agent.model)
claude claude-sonnet-4-6
>>> qc_agent.llm_provider, AGENT_CONFIG.provider
('openai', 'openai')
5.16.4. Application session recipe#
Desktop or web applications should configure agents once from their settings layer, then create agents from controllers or service classes:
>>> from pycsamt.agents import AGENT_CONFIG
>>> def configure_agent_session(settings):
... if settings.llm_enabled:
... AGENT_CONFIG.configure(
... provider=settings.provider,
... api_key=settings.api_key,
... model=settings.model,
... )
... if settings.budget_usd:
... AGENT_CONFIG.set_budget(usd=settings.budget_usd)
... else:
... AGENT_CONFIG.reset()
Called once at startup with the application’s own settings object –
here stood in for by a minimal stand-in with the same four attributes –
it leaves AGENT_CONFIG ready for every controller or service class
constructed afterward:
>>> from types import SimpleNamespace
>>> settings = SimpleNamespace(
... llm_enabled=True,
... provider="openai",
... api_key=runtime_secret,
... model="gpt-4o-mini",
... budget_usd=3.0,
... )
>>> configure_agent_session(settings)
>>> AGENT_CONFIG.provider, AGENT_CONFIG.model, AGENT_CONFIG.remaining_usd
('openai', 'gpt-4o-mini', 3.0)
5.17. Troubleshooting#
Symptom |
Likely cause |
Fix |
|---|---|---|
|
No API key was resolved, or the provider call failed. |
Check |
Agent uses Claude even after configuring OpenAI |
The agent was constructed before the global switch, or it received an explicit constructor override. |
Switch provider before creating the agent; recreate existing agents. |
Agent built with |
|
Pass an explicit |
Environment key is ignored |
The active provider does not match the environment variable you set. |
Use |
|
The previous workflow already used the budget cap. |
Call |
Cost estimate looks wrong for a new model |
The model is not in the built-in rate table and pycsamt used a fallback. |
Add an explicit rate with |
A provider client import fails |
The optional provider package is not installed. |
Install only the needed client library, for example |
5.18. Configuration checklist#
Before running a large agent workflow:
confirm whether the workflow should use LLM assistance or deterministic mode;
select the provider and model explicitly;
keep secrets in environment variables or an application settings layer;
inspect
AGENT_CONFIG.info()once before creating agents;set a budget cap for exploratory notebooks or user-facing applications;
add custom rates for newly released models;
construct agents after the desired provider/model has been selected;
reset configuration at the end of tests to avoid leaking state between cases.