91 Commits

Author SHA1 Message Date
Yijia-Xiao
030b434585 fix(agents): stop priming tool calls in schema-only structured agents
- with_structured_output binds a single tool (the schema), so a primed model emitted
  an unknown web_search call and the attempt was discarded for a free-text retry,
  costing an extra round trip and the typed output
- drop the tool-range wording from the no-tool sentiment analyst and state the
  constraint once via a shared NO_EXTERNAL_TOOLS #1130
2026-07-18 06:28:38 +00:00
Yijia-Xiao
3f6c082695 fix(cli): report an unusable terminal instead of a prompt_toolkit traceback
- Windows terminals without a console buffer raised NoConsoleScreenBufferError
  before the first prompt, surfacing a raw traceback with no guidance
- gate the Windows-only import on sys.platform so a broken prompt_toolkit still
  surfaces there, and the handler stays inert on other platforms #1138
2026-07-18 06:28:38 +00:00
Yijia-Xiao
d78c698d0e fix(dataflows): refresh the same-day OHLCV cache
- the per-day cache was reused unconditionally, so a run started before the day's
  bar was final served that snapshot to every later run, feeding a stale close
  into technical analysis
- a present row is not sufficient either, since Yahoo publishes a partial intraday
  candle; a TTL now governs every current-day cache while historical caches stay
  immutable #1150
2026-07-18 06:28:38 +00:00
Yijia-Xiao
40774ca042 fix(dataflows): make the Yahoo news window UTC and end-exclusive
- the upper bound was inclusive, so an article stamped exactly midnight after
  end_date leaked into a historical run
- flat epoch timestamps were parsed in host-local time and offset-aware stamps had
  tzinfo stripped without converting, making filtering machine-dependent
- normalize every operand to UTC and use a half-open [start, end + 1 day) #1126
2026-07-18 06:28:38 +00:00
Yijia-Xiao
0f70af2f31 feat(llm): add Claude Sonnet 5 and Fable 5 to the catalog
- refresh the Anthropic lineup to the current GA set (Fable 5, Opus 4.8,
  Sonnet 5, Opus 4.7, Haiku 4.5)
- extend the effort gate to single-number Claude 5 IDs (claude-sonnet-5,
  claude-fable-5) so their effort setting is honored
2026-07-05 14:29:07 +00:00
Yijia-Xiao
43bd32befa feat(llm): support Bedrock API-key auth via AWS_BEARER_TOKEN_BEDROCK
- pass the token to ChatBedrockConverse as api_key so langchain-aws prefers bearer
  auth and an ambient AWS_PROFILE can't override it; no AWS access keys required #1103
2026-07-05 14:29:07 +00:00
Yijia-Xiao
a102afa090 fix(dataflows): map crypto to StockTwits/Reddit sentiment symbols
- crypto reached StockTwits as Yahoo's BTC-USD (404) instead of BTC.X, and Reddit
  searched the dashed pair that barely matches; both now resolve the base via a
  shared crypto_base() helper, restoring crypto sentiment
- also fixes a StockTwits resilience test class that pytest never collected #1113
2026-07-05 14:29:07 +00:00
Yijia-Xiao
daf1da9c35 fix(graph): key checkpoints on graph shape and expose the LLM retry budget
- checkpoint resume keyed only by ticker+date silently continued the old graph
  under a different analyst selection / depth / asset mode; fold a run signature
  into the thread id #1089
- add llm_max_retries + TRADINGAGENTS_LLM_MAX_RETRIES, forwarded to every provider
  when set (int-coerced, rejects negatives/booleans), so a 429 burst can't abort
  a run #1091
2026-07-05 14:29:07 +00:00
Yijia-Xiao
b47a828a4f fix(graph): give the shared debate/risk routers a complete path_map
- should_continue_debate (2 edges) and should_continue_risk_analysis (3 edges)
  each returned more targets than any one edge mapped; a fall-through under
  prompt/i18n/refactor drift crashed LangGraph mid-run
- share a complete DEBATE_PATH_MAP / RISK_ANALYSIS_PATH_MAP across every edge #1088
2026-07-05 14:29:07 +00:00
Yijia-Xiao
622f99d28a fix(analysts): align the news prompt with the get_news tool signature
- prompt advertised get_news(query, ...) but the tool takes a ticker, so the
  model hallucinated free-text query calls
- advertise get_news(ticker, start_date, end_date) #1116
2026-07-05 14:29:07 +00:00
Yijia-Xiao
3570f2e1e6 fix(dataflows): apply the Alpha Vantage fundamentals look-ahead filter
- the payload is a JSON string, so the dict-only guard skipped filtering and
  future-dated reports leaked into historical runs, breaking the #475 guarantee
- parse before filtering; non-JSON bodies and an unset curr_date pass through #1115
2026-07-05 14:29:06 +00:00
Yijia-Xiao
8ab24f30af test: make the API-key fixture robust to empty-string env vars
A key left blank in a .env (var present but empty) bypassed the placeholder,
so local runs diverged from CI. Use 'or' instead of a .get default.
2026-06-21 23:50:33 +00:00
Yijia-Xiao
a0120e1805 feat(reporting): share the report-tree writer between the CLI and the API
The per-section markdown report tree was written only by the CLI, so programmatic
(TradingAgentsGraph) runs produced no saved reports.

- Extract the writer into tradingagents/reporting.write_report_tree.
- The CLI's save_report_to_disk delegates to it (no behavior change).
- Add TradingAgentsGraph.save_reports(final_state, ticker) so headless/API callers
  get the same report tree, defaulting under results_dir.
2026-06-21 23:22:30 +00:00
Yijia-Xiao
ec3974b84e chore(config): remove the no-op analyst_concurrency_limit knob
The knob was accepted but inert — analysts run strictly sequentially and the
value was never used. Remove it rather than ship a misleading config key.
Parallel analyst execution is tracked for v0.3 (#634/#671/#487).
2026-06-21 22:31:35 +00:00
Yijia-Xiao
0405168f20 fix(schema): coerce null-ish strings in optional float fields
A weak model can write a placeholder ('None', 'N/A') into an optional price
field, tripping schema validation. Coerce null-ish strings to None on the
trader/PM float fields; real numeric strings still parse.
2026-06-21 22:31:35 +00:00
Yijia-Xiao
517eeaf4b9 fix(structured): harden structured output for local servers and thinking models
- Local servers (LM Studio, vLLM) reject the object-form tool_choice langchain
  sends for function calling. The generic openai_compatible provider now binds
  the schema as a tool without forcing tool_choice.
- A structured call can return no parsed result (a thinking model answering in
  plain text); fall back to free text with a clear reason instead of an opaque
  render error.
2026-06-21 22:09:43 +00:00
Yijia-Xiao
9ad98c55c5 fix(data): normalize ticker on the news path
The yfinance news fetch queried the raw ticker while every other path uses the
canonical symbol, so broker/forex/crypto aliases silently returned no news.
Normalize it (XAUUSD -> GC=F) and keep the user's ticker in the report header.
2026-06-21 21:28:59 +00:00
Yijia-Xiao
ee1ece3347 fix(dataflows): degrade gracefully when an optional vendor fails
Optional enrichment vendors (FRED macro, Polymarket events) raised on a bad LLM
indicator, a missing key, or a network blip, which aborted the whole run.

- Router: mark macro_data and prediction_markets optional; a sole-vendor failure
  returns a sentinel instead of re-raising. Core categories still raise.
- FRED: reject a descriptive phrase up front and return guidance instead of
  400ing the API; an unknown series returns a not-found message, not a crash.
2026-06-21 21:28:59 +00:00
Yijia-Xiao
7bb16c5daa chore(models): retire deprecated models, simplify thinking config
Trim each provider to current-generation models and drop the special-casing
they required:

- OpenAI: remove gpt-4.1 (deprecated; the only non-reasoning model).
- Anthropic: remove Claude Sonnet 4.5 (legacy; the only Sonnet that 400s on effort).
- Google: remove the Gemini 2.5 line (superseded by 3.x).
- Gemini client: drop the integer thinking_budget mapping; 3.x takes the string
  thinking_level directly.

Effort/reasoning gates stay as defense in depth for custom model IDs. All kept
IDs verified against live APIs.
2026-06-21 21:03:05 +00:00
Yijia-Xiao
a420ad0f3b fix(cli): honor env precedence for LLM and run config
Interactive selections and flag defaults overrode TRADINGAGENTS_* env vars.
Rule: an explicit env value or CLI flag wins; otherwise the env-applied
default is kept.

- Research depth: skip the prompt when both round-count env vars are set, and
  stop overwriting them (#977).
- Checkpoint: --checkpoint/--no-checkpoint is tri-state; omitting it keeps
  TRADINGAGENTS_CHECKPOINT_ENABLED (#976).
- Docker ollama: use TRADINGAGENTS_LLM_PROVIDER + OLLAMA_BASE_URL, not a bare
  LLM_PROVIDER the overlay never reads (#975).
- Reasoning/thinking knobs: settable via env; the prompt is skipped when set.
- Effort gating: forward effort only to models that accept it (Anthropic
  Opus 4.5+/Sonnet 4.6+, OpenAI reasoning models); drop it elsewhere.
- Boolean env values: raise a named error on invalid input instead of
  silently becoming False.
2026-06-21 21:03:05 +00:00
Yijia-Xiao
c15200dc28 fix(cli): label OpenRouter prompts and shortlist mainstream models
Label each OpenRouter model prompt by mode (quick/deep) like the other
providers, so the two consecutive selections are distinguishable. Populate the
dropdown with the newest models from mainstream chat providers rather than the
universal-newest (which surfaced niche/experimental releases); Custom ID still
reaches anything. Cancelled required prompts now exit cleanly instead of
crashing, and the output-language prompt falls back to English.
2026-06-14 18:49:02 +00:00
Yijia-Xiao
03600f3121 chore(models): refresh the model catalog to current provider lineups
Verified each provider's hard-coded list against current official docs:
- MiniMax: add MiniMax-M3 (1M ctx, multimodal) as the default; keep M2.7 line.
- Qwen: use the live qwen{3.7,3.6}-{plus,max} IDs.
- GLM: add glm-5.2 as the latest flagship.
- xAI: drop deprecated grok-4-fast-* / grok-4-0709 builds.
- DeepSeek: migrate to deepseek-v4-pro / deepseek-v4-flash (the chat/reasoner
  aliases are deprecated 2026-07-24 and now map to V4 Flash).
OpenAI, Anthropic, and Gemini were already current and are unchanged.
2026-06-14 17:03:17 +00:00
Yijia-Xiao
e3bc872982 chore(lint): make the repository ruff-clean under the strict select
Clear the deferred full-repo lint backlog so the whole tree passes the strict
ruff select (E,W,F,I,B,UP,C4,SIM). Mechanical fixes dominate: import sorting,
pep585/604 annotations, dropped dead imports, and whitespace. The few semantic
changes are behavior-preserving: declare __all__ on the agent_utils and
alpha_vantage re-export hubs; expand 'from x import *' to explicit names; use
immutable tuple defaults instead of mutable list defaults; contextlib.suppress
for try/except/pass; and narrow an over-broad assertRaises.
2026-06-14 16:38:36 +00:00
Yijia-Xiao
cbc5f67d42 test(i18n): guard that every report agent applies the output language
The output-language instruction is applied across all report-producing agents
(analysts, researchers, risk debators, research manager, trader, portfolio
manager), but nothing enforced it, so agents had silently dropped it before. Add
a parametrized guard asserting each report agent calls get_language_instruction()
so a non-English run stays fully localized and the regression can't recur.
2026-06-14 15:56:59 +00:00
Yijia-Xiao
3cddf1e331 fix(llm): use the OpenAI Responses API only for native endpoints
The Responses API exists only on native OpenAI. When the openai provider is
pointed at a custom base_url (a proxy, gateway, or local server that speaks only
Chat Completions), keep the Responses API off so the call does not fail.
2026-06-14 07:23:19 +00:00
Yijia-Xiao
308757c999 fix(data): catch http.client transport errors in StockTwits
A truncated/incomplete chunked response raises http.client exceptions
(IncompleteRead/BadStatusLine) that are not OSErrors, so they bypassed the
existing handler and crashed the analysis. Broaden the catch so the fetch
degrades to its placeholder string like every other transport failure.
2026-06-14 07:23:19 +00:00
Yijia-Xiao
eeb84aa63b fix(reddit): go RSS-first with 429 backoff and robust transport errors
The JSON search endpoint is reliably WAF-blocked (403) for public clients, so
probing it on every call doubled request volume against Reddit's per-IP rate
limit and tripped 429 on the RSS fallback, blanking the sentiment feed. Fetch
the Atom/RSS feed directly (JSON kept as an opt-in path that still degrades to
RSS on 403), back off once on a 429 honouring Retry-After, and pace requests a
little wider. Also broaden the error handling to catch http.client chunked
transfer errors (IncompleteRead/BadStatusLine) alongside OSError, which on their
own slipped through and crashed the pipeline.
2026-06-14 07:23:19 +00:00
Yijia-Xiao
9fd54f8368 fix(data): reject stale yfinance OHLCV instead of reporting wrong prices
yfinance intermittently returns a year-old partial frame (e.g. June 2025 rows
for a June 2026 request) that still has rows and a Close, so it passed the
empty-check and silently fed a wrong close price and indicators into the report
(#1021). Add a freshness guard that rejects a frame whose latest row is far
older than the requested date, on both the raw OHLCV path and the indicator
path. It raises the existing NoMarketDataError with a stale-specific detail, so
the vendor router's try-next-vendor and single unavailable-signal handling apply
unchanged; the sentinel now surfaces that detail so the agent reports the
specific reason rather than fabricating a value.
2026-06-14 07:10:15 +00:00
Yijia-Xiao
7df18fc912 refactor(data): unify vendor errors under a VendorError hierarchy
Every condition where a vendor cannot return usable data now derives from a
single VendorError base (errors.py): NoMarketDataError, VendorRateLimitError,
and VendorNotConfiguredError (still a ValueError for back-compat). Vendor-named
errors subclass the generic bases, and the router catches the base types, so a
new vendor needs no new except clause. Not-configured now has explicit
try-next-vendor handling instead of falling through the generic catch-all. The
number of error types tracks the number of distinct router reactions, not the
number of causes.
2026-06-14 07:10:15 +00:00
Yijia-Xiao
db059034a2 feat(data): add Polymarket prediction markets as a keyless vendor
Surface live, market-implied probabilities for forward-looking events (Fed
decisions, recession, elections, geopolitics, crypto) to the news analyst via a
new get_prediction_markets tool and a prediction_markets vendor category. Backed
by Polymarket's public Gamma API (no key). Results are filtered to open,
forward-looking markets (closed and past-dated events excluded), ranked by
traded volume, and rendered with implied probability, volume, resolution date,
and the recent move. External errors degrade to a clear unavailable message
rather than interrupting the analyst.
2026-06-14 06:30:43 +00:00
Yijia-Xiao
ddfb840ecf feat(data): add FRED macro indicators as an optional vendor
Surface Federal Reserve Economic Data (rates, inflation, labor, growth) to the
news analyst via a new get_macro_indicators tool and a macro_data vendor
category. Friendly aliases (cpi, unemployment, fed_funds_rate, 10y_treasury,
yield_curve, ...) map to FRED series IDs; raw series IDs are accepted too. The
report gives the latest value, change over the window, and a recent observation
table. Windowing is lookahead-safe (observation_end = curr_date), missing values
are skipped, and a missing FRED_API_KEY surfaces as a clear not-configured
condition through the vendor router rather than a crash.
2026-06-14 06:08:31 +00:00
Yijia-Xiao
895ed130f9 feat(llm): add Amazon Bedrock as a first-class provider
Bedrock uses the Converse API (langchain-aws) and the AWS credential chain, so
it has its own client like Anthropic/Google rather than the OpenAI-compatible
registry. langchain-aws is an optional dependency (pip install ".[bedrock]"),
lazy-imported with a clear install hint; importing the package never requires
it. The model name is a Bedrock model ID / inference profile ID.
2026-06-14 04:24:54 +00:00
Yijia-Xiao
295e84cd54 feat(llm): add NVIDIA NIM, Kimi, Groq, and Mistral providers
Each is a one-row entry in the OpenAI-compatible provider registry (base_url,
key env, CLI option); the model is user-specified since they serve many models.
2026-06-14 04:13:39 +00:00
Yijia-Xiao
20d3b0782f feat(llm): unify OpenAI-compatible providers behind a registry + generic endpoint
The OpenAI-compatible family (openai, xAI, DeepSeek, Qwen, GLM, MiniMax,
OpenRouter, Ollama) all speak the same Chat Completions API and differ only by
base_url, key, and two narrow wire-format quirks already isolated in subclasses.
Replace the scattered base-URL dict, key handling, and client-class branches with
one ProviderSpec registry that get_llm and the factory drive off; provider quirks
stay in their subclasses. Add a generic "openai_compatible" provider for any
OpenAI-compatible server (vLLM, LM Studio, llama.cpp, relays) via backend_url +
optional key — adding a provider is now one registry row. Native Anthropic/Google
keep their own clients (genuinely different APIs). Also fixes the env backend URL
being ignored when the provider was chosen interactively (#978).
2026-06-14 03:22:24 +00:00
Yijia-Xiao
4e7821d574 fix(graph): register get_verified_market_snapshot in the market ToolNode
The market analyst is bound to call get_verified_market_snapshot and its prompt
requires it as the source of truth, but the tool was missing from the market
ToolNode executor — so the call failed and the model reported it "unavailable"
and skipped verification. Register it (with a regression guard) so the snapshot
actually runs and grounds the report.
2026-06-14 02:46:29 +00:00
Yijia-Xiao
0c1231a405 fix(data): keep future/undated news out of historical windows
The yfinance news date filter only ran when an article had a parsed date, so
flat-format and undated articles bypassed it and leaked future news into
historical/backtest runs. Parse the flat providerPublishTime, apply one
look-ahead-safe window rule across ticker and global news (undated kept only
when the window reaches the present), and return an informative message when
everything is filtered out.
2026-06-13 21:54:07 +00:00
Yijia-Xiao
e4be7cc5a3 fix(data): add Alpha Vantage request timeout and stop mislabeling bad keys
Alpha Vantage requests had no timeout (a stall could hang the run) and any
notice mentioning "API key" was raised as a rate limit — so an invalid/missing
key was mislabeled and silently treated as transient. Add a 30s request timeout
and classify rate-limit phrasing before key errors (rate-limit notices also
mention "API key"), surfacing a bad key as a real configuration error.
2026-06-13 21:47:06 +00:00
Yijia-Xiao
dab07688fb fix(data): include the requested end date in yfinance fetches
yfinance treats end as exclusive, so get_YFin_data_online dropped the requested
end_date row and load_ohlcv dropped the current day. Request one day past the
end so the range is inclusive (look-ahead is still prevented by the curr_date
filter; the header still shows the requested range). Also correct the load_ohlcv
docstring to the 5-year window it actually downloads.
2026-06-13 21:30:11 +00:00
Yijia-Xiao
65608831f8 fix(data): respect the configured vendor chain and log vendor failures
The router silently extended every request to all available vendors regardless
of config, so an explicit single-vendor choice still fell back to others and
returned data from an unexpected source (#988, #289), and serious primary-vendor
errors were swallowed without a trace (#989). The configured vendor list is now
the exact chain (list several for ordered fallback; "default" uses all), unknown
vendors raise, and swallowed vendor errors are logged. Adds an autouse config
isolation fixture so vendor config can't leak between tests.
2026-06-13 21:11:25 +00:00
Yijia-Xiao
76add9048f fix(cli): unify ticker handling with the data-path symbol normalizer
The CLI validated, normalized, and classified tickers with its own logic that
diverged from the data layer: it rejected '=' symbols like GC=F (#980),
classified BTCUSD as a stock (#981), and accepted unpriceable BTC-USDT (#982).
Route the CLI through normalize_symbol (now mapping USDT/USDC crypto quotes to
Yahoo's -USD pair), so validation, classification, and pricing agree.
2026-06-13 20:50:21 +00:00
Yijia-Xiao
7c8fe2fe9f fix(data): normalize symbols on the identity and reflection paths
resolve_instrument_identity and the reflection return lookup queried Yahoo with
the raw ticker, so broker/forex/commodity symbols (XAUUSD, BTCUSD, EURUSD)
failed identity or could mismatch the priced instrument even though the price
path already normalized them. Route both through normalize_symbol (#983, #984).
2026-06-13 20:39:52 +00:00
Yijia-Xiao
2e67782f20 feat(cli): skip interactive LLM selection when configured via environment (#873)
Setting the LLM env vars now skips the matching CLI selection step and uses
the value, so OpenAI-compatible endpoints (opencode, LM Studio, etc.) and
unattended runs work without prompting. Unset vars are chosen interactively
as before.

  TRADINGAGENTS_LLM_PROVIDER -> skips provider step (still verifies API key)
  TRADINGAGENTS_LLM_BACKEND_URL -> custom endpoint (else provider default)
  TRADINGAGENTS_DEEP_THINK_LLM / _QUICK_THINK_LLM -> skips model step
  TRADINGAGENTS_OUTPUT_LANGUAGE -> skips language step

Builds on the existing TRADINGAGENTS_* config overrides (which already feed
DEFAULT_CONFIG); this wires the CLI to honor them instead of re-prompting.
2026-05-31 22:38:48 +00:00
Yijia-Xiao
1ff3f07a73 fix: support commodity/forex/crypto tickers and never invent prices (#781)
Analyzing a symbol Yahoo Finance does not recognize (e.g. XAUUSD+) could
produce an invented price instead of an error. The agent now either prices
the correct instrument or clearly reports that data is unavailable.

Ticker support:
- Commodities/forex/crypto resolve to the symbol Yahoo actually serves, so
  you can enter the common form and it just works:
    XAUUSD / XAUUSD+ / GOLD  -> GC=F   (gold)
    USOIL                    -> CL=F   (WTI crude)
    EURUSD                   -> EURUSD=X
    BTCUSD                   -> BTC-USD
    SPX500 / NAS100          -> ^GSPC / ^NDX
  Native Yahoo symbols (AAPL, GC=F, ^GSPC) keep working unchanged. New
  instruments are added by extending the alias table.

Reliability:
- Unknown or delisted symbols now return a clear "data unavailable" result
  the agent reports verbatim, instead of a value the model fills in.
- A failed fetch no longer leaves a broken symbol cached until the cache is
  cleared by hand.
2026-05-31 22:38:47 +00:00
Yijia-Xiao
2f85be624e chore(llm): add latest models and default to GPT-5.5
Add Claude Opus 4.8, Gemini 3.5 Flash, Grok 4.3, and Qwen3.7-Max; default
deep model is now GPT-5.5.
2026-05-31 08:01:03 +00:00
Yijia-Xiao
c93b92c7a4 feat(markets): add China A-share benchmarks and document non-US tickers
A-shares already resolve through the Yahoo Finance vendor (Shanghai .SS,
Shenzhen .SZ) with correct identity and indicators; add the SSE/SZSE
composite benchmarks so their alpha isn't measured against SPY, and
document the exchange-suffix tickers we support (incl. A-shares, crypto).
2026-05-31 07:29:19 +00:00
Yijia-Xiao
8694bd070d fix(llm): send MiniMax reasoning_split via extra_body so the openai SDK accepts it (#826) 2026-05-31 06:20:55 +00:00
Yijia-Xiao
2c9f1bfe65 fix(cli): consolidate duplicate get_ticker and only announce non-stock asset type 2026-05-31 06:13:35 +00:00
Yijia-Xiao
8a22594607 feat(config): expose sampling temperature and document reproducibility
Adds a cross-provider temperature config (and TRADINGAGENTS_TEMPERATURE),
forwarded to every LLM client when set, so runs can be made less variable
on models that honor it. Adds a README "Reproducibility" section that
separates the sources of run-to-run variation, what users can control
(temperature, non-reasoning model, pinned date), and what is inherent to
LLM-driven analysis, and notes that the identity and verified-data fixes
already removed the "different companies / fabricated prices" variance.

#178 #168
2026-05-31 03:51:50 +00:00
Yijia-Xiao
47cbb321fe feat(market): verified market-data snapshot to ground numeric claims
The market analyst could confabulate exact figures — citing a Bollinger
band or a "historically validated bounce" the data doesn't support (#830).
Add a deterministic get_verified_market_snapshot tool (latest OHLCV row,
common indicators, recent closes) the analyst must consult and treat as
the source of truth for any exact price/indicator claim, and instruct it
not to assert historical validation or support bounces without tool-backed
dates and prices.

#830
2026-05-31 01:58:32 +00:00
Yijia-Xiao
e80636fc0e feat(sentiment): structured output for the Sentiment Analyst
The analyst emitted free-form prose, so its sentiment header varied by
provider and run and downstream consumers needed drifting regex. Extend
the structured-output pattern the trio already uses: a SentimentReport
schema (band + 0-10 score + confidence + narrative) rendered to a
deterministic header, with a free-text fallback for providers that lack
native structured output.

#796
2026-05-31 01:45:25 +00:00