Commit graph

2708 commits

Author SHA1 Message Date
mateo-berri
9b8ad46f37 fix(openai): flatten top-level anyOf/oneOf/allOf in Responses API tool schemas
OpenAI's function-calling validator rejects tool parameters carrying
oneOf/anyOf/allOf/enum/const/not at the top level, while the ChatGPT
backend Codex talks to natively accepts them, so an MCP tool declaring a
top-level union 400s through the proxy. Merge the branches into the
object schema for OpenAI itself only, walking the namespace-nested tools
current Codex builds send, on both /v1/responses and /v1/responses/compact
2026-08-29 13:11:04 -07:00
Mateo Wang
9ed7de6c02
Merge pull request #38670 from BerriAI/devin_ai_38659_cohere_embed_dispatch
fix(bedrock): route all cohere.embed models to the cohere embedding config
2026-08-29 12:55:32 -07:00
Mateo Wang
817bbe1dc6
Merge pull request #34440 from dan2k3k4/litellm_soniox_srt_cue_grouping
fix(soniox): align synthesized SRT/VTT cues to real speech timing
2026-08-29 12:49:57 -07:00
mateo-berri
f4b5449c6a fix(openai_like): strip cache_control ttl before forwarding /v1/messages to non-Anthropic providers 2026-08-29 12:48:04 -07:00
Mateo Wang
c453920f7a
Merge pull request #38285 from BerriAI/litellm_azure_v1_image_routes
fix(azure): use /openai/v1 image routes for v1, preview and latest api versions
2026-08-29 12:45:37 -07:00
mateo-berri
886d39c3a2 test(bedrock): expect cohere embed base64 encoding_format to normalize to float 2026-08-29 12:44:01 -07:00
mateo-berri
a007fa49e5 Merge branch 'litellm_internal_staging' into litellm_veo_31_lite 2026-08-29 12:04:42 -07:00
mateo-berri
4c42c01cb2 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_soniox_srt_cue_grouping
# Conflicts:
#	litellm/llms/soniox/common_utils.py
2026-08-29 12:02:21 -07:00
Devin AI
23703a5341 Merge remote-tracking branch 'origin/litellm_internal_staging' into devin/1787944648-registry-audit-rolling 2026-08-29 19:02:03 +00:00
mateo-berri
9e01bd1441 fix(azure): send the deployment name as the body model on v1 image routes 2026-08-29 11:41:58 -07:00
mateo-berri
68f891fd2b Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_keyless_key_managed_resource_owner 2026-08-29 11:33:08 -07:00
Mateo Wang
c42ac262d3
Merge branch 'litellm_internal_staging' into fix_databricks_oauth_url 2026-08-29 07:00:58 -07:00
mateo-berri
163c7033d6 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_decrease_anys_opus5 2026-08-29 06:38:33 -07:00
mateo-berri
c37260a2bd Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_decrease_anys_opus5
# Conflicts:
#	basedpyright-code-budget.json
#	enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py
#	litellm/_lazy_imports.py
#	litellm/a2a_protocol/litellm_completion_bridge/transformation.py
#	litellm/integrations/bitbucket/bitbucket_client.py
#	litellm/integrations/compression_interception/handler.py
#	litellm/integrations/prometheus_helpers/prometheus_api.py
#	litellm/litellm_core_utils/model_response_utils.py
#	litellm/litellm_core_utils/url_utils.py
#	litellm/llms/anthropic/experimental_pass_through/context_management/dispatcher.py
#	litellm/llms/anthropic/experimental_pass_through/responses_adapters/handler.py
#	litellm/llms/anthropic/skills/transformation.py
#	litellm/llms/azure/files/handler.py
#	litellm/llms/bedrock/realtime/handler.py
#	litellm/llms/chatgpt/chat/streaming_utils.py
#	litellm/llms/compactifai/chat/transformation.py
#	litellm/llms/oci/chat/cohere.py
#	litellm/llms/vertex_ai/vector_stores/rag_api/transformation.py
#	litellm/proxy/agent_endpoints/agent_registry.py
#	litellm/proxy/client/cli/commands/credentials.py
#	litellm/proxy/client/cli/commands/teams.py
#	litellm/proxy/common_utils/get_routes.py
#	litellm/proxy/db/routing_prisma_wrapper.py
#	litellm/proxy/guardrails/guardrail_hooks/custom_code/sandbox.py
#	litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py
#	litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py
#	litellm/proxy/guardrails/guardrail_hooks/promptguard/promptguard.py
#	litellm/rust_bridge/responses_websocket.py
#	litellm/secret_managers/secret_manager_handler.py
#	ruff-strict-budget.json
#	type-discipline-budget.json
2026-08-29 06:37:10 -07:00
Mateo Wang
e48f8f016f
Merge pull request #38148 from mubashir1osmani/litellm_hosted_vllm_videos
feat(hosted_vllm): add vLLM-Omni videos API
2026-08-29 06:30:35 -07:00
Devin AI
f0849eb0c9 fix(models): xai retirement repricing, bedrock grok-4.6 caching, openai/gemini deprecation dates
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-29 13:11:26 +00:00
mateo-berri
8d4620649f Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_decrease_anys_opus5
# Conflicts:
#	basedpyright-code-budget.json
#	litellm/caching/valkey_semantic_cache.py
#	litellm/integrations/compression_interception/handler.py
#	litellm/integrations/custom_logger.py
#	litellm/llms/custom_httpx/container_handler.py
#	litellm/llms/infinity/rerank/transformation.py
#	litellm/proxy/agent_endpoints/agent_registry.py
#	litellm/repositories/base_repository.py
#	litellm/repositories/credentials_repository.py
#	litellm/repositories/team_repository.py
#	ruff-strict-budget.json
#	type-discipline-budget.json
2026-08-29 06:03:33 -07:00
Devin AI
401b12e64b Merge remote-tracking branch 'origin/litellm_internal_staging' into devin/1787944648-registry-audit-rolling 2026-08-29 13:02:37 +00:00
Mateo Wang
6a3333d3c8
Merge pull request #38747 from BerriAI/litellm_aws_partition_helper
fix(aws): build every AWS endpoint and ARN from the region's partition (aws-cn, aws-us-gov)
2026-08-29 04:10:59 -07:00
Mateo Wang
39e4f1ae13
Merge pull request #38727 from BerriAI/litellm_aws_external_id_embed_sagemaker
fix(aws): forward aws_external_id in Bedrock embeddings and SageMaker credential loading
2026-08-29 03:30:32 -07:00
Devin AI
db1b1e2195 fix: bound Hugging Face config fetch and keep embedding tests off the network
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-29 09:45:52 +00:00
mateo-berri
1947c65081 test(aws): type the new partition test parameters 2026-08-29 02:27:18 -07:00
mateo-berri
7fbcd9c3ed fix(bedrock): treat partial record counts as unknown on batch retrieve 2026-08-29 01:40:07 -07:00
mateo-berri
ad8c1457d1 fix(aws): build every AWS endpoint and ARN from the region partition
Adds litellm/litellm_core_utils/aws_partition.py mapping a region to its
AWS partition (aws, aws-cn, aws-us-gov, and the iso partitions), its DNS
suffix, and its ARN prefix, and uses it at every AWS host and ARN build
site: bedrock (runtime, agent, agentcore, legacy client, batches, files,
realtime), sagemaker, polly, secrets manager, s3 log uploads, bedrock
passthrough routes, and rag ingestion. ARN detection now accepts
arn:aws-cn: and arn:aws-us-gov: prefixes.

STS region resolution now falls back to the configured aws_region_name
after the aws_sts_endpoint host and the AWS_REGION/AWS_DEFAULT_REGION env
vars, so cn and gov role assumption no longer silently signs against
us-west-2.

A partition sweep test walks every endpoint builder with cn regions and
asserts no amazonaws.com host or arn:aws: prefix comes out, plus an AST
guard that fails on any new f-string hardcoding either literal.
2026-08-29 01:21:59 -07:00
mateo-berri
5d34fb20ff fix(bedrock): map real batch record counts and guard zero-count retire 2026-08-29 01:05:36 -07:00
mateo-berri
1a26608769 feat(vertex_ai): route gemini transcribe models to generateContent on /v1/audio/transcriptions 2026-08-29 00:46:03 -07:00
mateo-berri
44a5d7a47a fix(aws): forward aws_external_id in bedrock embeddings and sagemaker credential loading 2026-08-28 18:25:14 -07:00
mateo-berri
1a5a856e3e fix(guardrails): defer native /v1/messages stream logging until post_call scans finish 2026-08-28 16:05:45 -07:00
Mateo Wang
27c09248e4
Merge pull request #38593 from BerriAI/litellm_gpt5_default_reasoning_effort
fix(gpt-5): stop forwarding temperature and top_p to reasoning models that reject them
2026-08-28 15:20:41 -07:00
Mateo Wang
d4f6b4491d
Merge pull request #38656 from aaaaaandrew/litellm_preserve_stream_selected_model
fix(streaming): preserve provider model for cost calculation
2026-08-28 13:00:35 -07:00
Devin AI
9df3a4fc1c fix(model_prices): registry audit - add groq qwen3.8-27b, gemini live native audio (from #38573), anthropic sources
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-28 19:18:26 +00:00
yuneng-jiang
4d9025c2bf
Merge pull request #38677 from BerriAI/litellm_/mutmut-test-coverage-gaps-b4dd83
test: close mutation-testing gaps in container, skills and openai-like config factories
2026-08-28 10:58:16 -07:00
Mateo Wang
3a52ae0a3f
Merge pull request #38615 from BerriAI/litellm_mantle_gpt5_cost_tiers
fix(model_prices): add bedrock_mantle gpt-5.5/5.4 272K tiers, align sol with AWS invoice
2026-08-28 10:09:52 -07:00
Yuneng Jiang
0ec619e7d1
test: close mutation-testing gaps in container, skills and openai-like config factories
Mutation testing surfaced three factory functions whose tests ran against them
but asserted nothing that a mutation could break, so every planted bug survived.

- litellm/llms/litellm_proxy/skills/code_execution.py: the OpenAI and Anthropic
  tool schemas were unpinned (the Anthropic one was not reached by any test at
  all) and the handler's default fallbacks were unchecked
- litellm/containers/endpoint_factory.py: the endpoints.json contract, the
  generated sync/async function set and the response-type mapping were unpinned
- litellm/llms/openai_like/dynamic_config.py: the generated Responses API config
  class had no coverage of auth header, URL resolution or the store override

The openai_like tests clear _responses_config_cache around each test. Without
that, the module-level cache hands back a class built before the mutation and
the tests pass against mutated code.

Verified by re-running mutmut per scope:
  llms/litellm_proxy  45.2% -> 62.8%  (70 mutants newly killed)
  containers          36.8% -> 84.3%  (45 mutants newly killed)
  llms/openai_like    55.7% -> 66.9%  (34 mutants newly killed)
2026-08-28 09:28:14 -07:00
tin-berri
ca0b951a43
feat(spend): report prompt caching savings as total and gateway-attributed (#38134)
* feat(spend): report prompt caching savings as total and gateway-attributed

`prompt_caching_savings_spend` credited every cached request, including caching a
client asked for with its own `cache_control` and caching a provider does implicitly,
so the number overstated what the gateway had any hand in.

Gating that column in place would have fixed the overstatement by changing what the
column means, leaving rows written before the change saying "all caching savings" and
rows after saying "gateway-injected only" with nothing to tell them apart, and forcing
a decision about rewriting history. It also breaks the cache-leakage estimate on the
dashboard, whose numerator would be gated while its denominator, the cached token
counts, would not, so the rate it extrapolates from would be quietly diluted.

Report both instead. `prompt_caching_savings_spend` keeps meaning every net dollar
caching saved, which is what a customer means by "what did caching save me", and the
new `gateway_injected_caching_savings_spend` carries the subset litellm caused by
injecting the breakpoints itself. Both are derived from the same marker, so this
changes what is done with it rather than how it is obtained.

The attributed figure is normally the smaller of the two, being a subset of the same
requests, but not always: a request that writes cache it never reads has negative net
savings, and excluding such a request can lift the attributed figure above the total.

Also stops the marker riding into a fallback leg. The fallback rebuild spread the
failed attempt's metadata forward, so a deployment that injected nothing inherited the
marker and was credited anyway, which silently restored the very overstatement this
separates out.

* fix(bedrock): credit gateway caching where the tool cachePoint is placed (#38478)

The savings marker records breakpoints litellm placed, and a tool_config
injection point becomes one only in the converse transform, and only when the
request carries tools. The prompt hook cannot see either condition, so marking
on the point's presence credited request shapes that cached nothing, while
Bedrock tool caching the gateway did cause went uncredited.

Record it at the placement site instead. The marker's reader also resolves its
bucket by value now: litellm_params declares litellm_metadata as None on every
request, so asking the shared name resolver named a bucket that was not there
and the mark was dropped.
2026-08-28 00:19:06 -07:00
tin-berri
39e5b0c2d1
fix(anthropic): drop and self-heal empty thinking blocks on /v1/messages (#38625)
* fix(anthropic): drop and self-heal empty thinking blocks on /v1/messages

* test(anthropic): pin early-signature carry across the blank thinking chunk skip
2026-08-27 21:41:56 -07:00
Andrew Mattie
134a4cd9fd fix(streaming): preserve provider model for cost calculation 2026-08-27 23:26:00 -05:00
Yuneng Jiang
a4049b730c
fix(exceptions): keep a refused connection an APIConnectionError
#38318 taught exception_type to map upstream status codes for providers with
no branch of their own. It reads the status code off the exception, but
_handle_error stamps 500 onto every failure that never carried one, so a
refused connection reached the mapper wearing a status code nothing upstream
had sent, and came back as InternalServerError instead of APIConnectionError.

The two are not interchangeable to a caller: a 5xx says the provider answered
and failed, which the router treats as a reason to cool the deployment down,
while a connection error says the request never landed.

BaseLLMException now records whether its status code was received or
synthesized, _handle_error sets that when it invents the 500, and the status
mapper declines to act on a code litellm made up, so those failures fall
through to the APIConnectionError the branch was always meant to produce.

Genuine upstream 5xx responses are untouched, which the second test pins.
The search transformation assertion #38318 had loosened to InternalServerError
goes back to APIConnectionError for the same reason.
2026-08-27 21:00:24 -07:00
tin-berri
3300fc3a96
fix(moonshot, together_ai): send the reasoning effort Kimi K3 accepts (#38611)
* fix(moonshot, together_ai): send the reasoning effort Kimi K3 accepts

Moonshot documents reasoning_effort as a top-level chat completions field for its reasoning
models, and defaults it to max, but MoonshotChatConfig builds its supported params by
subtracting from the OpenAI base list, which never carried that param. An explicit level
raised UnsupportedParamsError before the request left the proxy, so low and high were
unreachable and every call ran at the provider default

Together accepts low, high and max on Kimi K3. The per-model clamp added for the gpt-oss
family folds max down to high for every model except deepseek-ai/DeepSeek-V4-Pro, so a caller
asking for max silently got roughly half the reasoning budget they paid for

Moonshot now offers reasoning_effort whenever the registry says the model reasons. Together
sends a level the map entry declares unchanged, and keeps its existing table for every level
an entry does not name, so the only value that moves is Kimi K3 at max

* fix(moonshot): unwrap the bridges' effort object to the level string
2026-08-27 20:46:46 -07:00
mateo-berri
837bcba32d fix(model_prices): add bedrock_mantle gpt-5.5/5.4 272K tiers, align sol with AWS invoice
AWS bills a Bedrock GPT-5.5 or GPT-5.4 prompt past 272K tokens under the long-context usage types for the
whole prompt, at 2x input, 2x cache read, and 1.5x output, and the cost map only had the flat rates, so a
300K prompt was logged at half of what the invoice charges. The map's promo rates for gpt-5.6-sol are 20%
under the $5.50 input, $33.00 output, $0.55 cache read, and $6.88 cache write per million the invoice bills.

Adds the *_above_272k_tokens fields to gpt-5.5 and gpt-5.4, moves sol's base and tier rates to the invoiced
ones, replaces the test that pinned the flat behaviour with one that pins the invoiced numbers, and updates
the sol pins in the mantle transformation tests
2026-08-27 19:31:45 -07:00
tin-berri
49e6081978
fix(anthropic): resolve /v1/messages effort tiers through the capability owner (#38492)
* fix(anthropic): resolve /v1/messages effort tiers through the capability owner

The bridge normalizer read three supports_*_reasoning_effort booleans of its own, so it
answered "which levels does this deployment take" independently of the resolver behind
/model_group/info. The two disagreed: a proxy advertising kimi-k3 max forwarded high.

Degrade against resolve_supported_reasoning_efforts instead, with the chains as a declared
table. When no step of a chain is accepted, the fallback is read off that same resolved set
rather than assumed, since an entry naming its levels outright can exclude the tiers the
per-level flags treat as unconditional. none is never chosen as that fallback, being an off
switch rather than a tier, and a deployment accepting no tier at all keeps the floor every
deployment degraded to before.

* test(anthropic): pin the normalized effort at the /v1/messages request boundary

The existing coverage stopped at normalize_reasoning_effort_value, so nothing failed if the
handler dropped or overwrote the normalized tier on its way into completion_kwargs. Drive
_prepare_completion_kwargs instead and assert on the kwargs handed to acompletion, in both the
string and the dict effort shapes, including the provider-prefixed model name the handler is
actually called with.

Against the pre-fix normalizer the fallback case fails, and against the baseline before a map
entry could declare its levels 7 of the 12 fail, so the boundary is pinned rather than restated.
2026-08-27 19:13:13 -07:00
Tin Chi Lo
e5c3df2da2 fix(gpt-5): resolve temperature support from the model's default reasoning effort
A gpt-5 model accepts a non-default temperature only while its effective reasoning
effort resolves to "none". litellm had no representation of the effort a model applies
when the request omits reasoning_effort, so it substituted supports_none_reasoning_effort,
which is a different fact. Every model that supports "none" without defaulting to it
therefore had temperature forwarded and rejected upstream, and because the carve-out
returned before the drop_params branch, drop_params: true could not save it.

Declare the fact instead. A new cost-map key, default_reasoning_effort, states the effort
the provider applies when the request omits one, and one shared predicate resolves the
effective effort from it: an explicit reasoning_effort wins, otherwise the declared
default, otherwise the catalogue decides.

That last step matters because the cost map is fetched from the published branch at import
time, so it can be OLDER than the code reading it. On such a map every model looks
undeclared, and reading that as "reasoning is active" would strip temperature from the 39
gpt-5.1/5.2/5.4 entries that accept it, a regression caused by data lag rather than by
anything about the model. So an absent declaration is only meaningful once the catalogue
carries the key at all; a map that predates the feature keeps the answer litellm gave
before it existed, and the conservative answer applies from the moment the data lands.

The top_p/logprobs/top_logprobs gate carried the same assumption spelled differently and
now shares the predicate, as does the Responses API, which reimplemented the rule and is
what the default /v1/messages bridge routes openai models through. Azure normalises its
routing names in one resolver that every capability lookup goes through, which replaces
its bespoke per-lookup rewrite.

Declared on the 37 gpt-5.1/5.2/5.4 entries measured to accept temperature=0 today, so
their behaviour is unchanged. The 23 gpt-5.5/5.6 entries that reject it stay undeclared
and are fixed once the catalogue carries the key.

Resolves LIT-3797
Resolves LIT-5028
2026-08-27 18:46:18 -07:00
Mateo Wang
10cd9259a3
Merge pull request #38100 from FelipeRodriguesGare/bugfix/tencent-thinking-extra-body
fix(tencent): route thinking through extra_body in chat completions
2026-08-27 18:23:11 -07:00
tin-berri
1ab6fd89d2
fix(anthropic): carry the effort tier only where the target declares reasoning_effort (#38592)
The /v1/messages bridge decided a Claude target could take `reasoning_effort` from
the model name, which says nothing about the params the provider in front of it
accepts. Snowflake serves Claude over the Anthropic dialect and declares `thinking`
alone, so `get_optional_params` raised `UnsupportedParamsError` before the request
reached the wire: every adaptive request carrying an effort tier turned a 200 into
a 400 for all seven of its Claude entries.

The tier is now offered only where the target declares the param, reading the same
`get_supported_openai_params` the sibling `_supports_prompt_cache_key` reads twelve
lines up. A target declaring neither carrier keeps its bare `thinking` block, which
is what this bridge sent before it carried a tier at all.

Without a resolved provider the tier stays behind rather than being offered blind.
Resolving one from the model's prefix instead would run an OAuth device flow for
github_copilot and chatgpt, blocking for minutes, and one of the two callers in that
position is a logging callback. The copilot case is pinned by a test.
2026-08-28 00:41:46 +00:00
Devin AI
eee47dcdaa fix(bedrock): share one item_id across a user utterance's realtime events
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-28 00:39:10 +00:00
Devin AI
5eee3bd9f9 fix(bedrock): dispatch success handlers for realtime sessions so spend is logged
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-28 00:28:22 +00:00
Devin AI
b7a7754b05 fix(bedrock): surface Nova Sonic user transcripts, speech events, and usage in realtime API
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-28 00:07:40 +00:00
mateo-berri
abbfdd5484 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_keyless_key_managed_resource_owner
Some checks failed
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
2026-08-27 16:59:30 -07:00
tin-berri
44d84360fb
fix(anthropic): carry the adaptive effort tier to every bridged Claude target (#38533)
/v1/messages forwarded `thinking` verbatim for a Claude-family model and then returned,
carrying `output_config.effort` only when the model string started with a Bedrock prefix.
Every other bridged provider got a bare adaptive thinking block, so the caller's effort did
nothing: max and minimal produced byte-identical upstream bodies.

Send those targets the tier as `reasoning_effort`, which is the param they take. Bedrock keeps
taking `output_config`, since the two are not interchangeable there: an application inference
profile ARN resolves to no chat config, so `reasoning_effort` is dropped and the tier vanishes,
and a provider that rebuilds `output_config` from it overwrites a caller-set `thinking.display`
on the way. The tier stays a plain string, the summary already travelling inside the forwarded
`thinking` block. Adaptive with no tier, and budgeted thinking, both stay exactly as they were.
2026-08-27 15:48:42 -07:00
tin-berri
30ff3723b2
feat(model_prices): let a map entry declare its exact reasoning_effort levels (#38481)
Kimi K3 accepts exactly low, high and max, defaults to max, and always thinks.
The map could not say that: medium and high have no supports_*_reasoning_effort
flag because every other reasoning model takes them, so the ten kimi-k3 entries
carried supports_reasoning alone and resolved to unknown. The dashboard then fell
back to a capability-blind level list that deliberately omits max, which is why a
kimi-k3 tier cannot be set to max thinking today.

Add reasoning_effort_levels, an array key in the shape the map already uses for
supported_endpoints and supported_modalities. Where present it is read first and
wins whole; every other entry keeps answering through the per-level flags,
unchanged. It is deliberately a different name from the computed
ModelGroupInfo.supported_reasoning_efforts, which stays derived from a group's
deployments and is never seeded from one deployment's model_info.

The levels are per entry rather than per model, because the deployments differ:
Moonshot, Together, Fireworks and Azure Foundry all forward the level unchanged
and get the model's own low/high/max, while Perplexity documents a six-value
enum it maps down internally and gets that. The /v1/messages degradation chain
consults the same declaration, so the level the map advertises is the level that
path forwards.
2026-08-27 15:38:01 -07:00