Summary
On the OpenRouter path we send provider: { "require_parameters": true } whenever the request body carries tools or a reasoning object (crates/buzz-agent/src/llm.rs:1558-1566). That flag is an assertion that every parameter in our body is honored by the endpoint OpenRouter routes to. But we build the body from our own capability model — not from the target model's advertised supported_parameters — so any parameter we send unconditionally is a latent hard 404 for whatever slice of the catalog does not advertise it.
The failure is not graceful. OpenRouter returns:
HTTP 404 {"error":{"message":"No endpoints found that can handle the requested parameters.","code":404}}
and openrouter_post maps any 404 to AgentError::LlmModelNotFound (llm.rs:1444-1449), which surfaces to the user as llm model not found: (<model>) 404: .... The message sends people hunting a model-name typo that does not exist — the model is present in the catalog and the key is valid.
Worth noting the diagnostic already exists one branch away: llm.rs:1487-1491 produces no OpenRouter endpoint supports the requested parameters — check model, effort, and tool requirements for a bare 503. The 404 that OpenRouter actually returns for this condition never reaches it, because the 404 arm returns first.
This was found while verifying #1975 against a live OpenRouter key. The largest instance (max_completion_tokens, 221/274 tools-capable models) is fixed on that branch. The pattern and the remaining instances are not.
The pattern
require_parameters: true is a promise about the whole body. We construct the body from what we support, and never intersect it against what the target model advertises. Every unconditional parameter is one un-advertised capability away from a 404 that reads as "model not found."
Naming the pattern matters more than the individual instances below — the next unconditional field added to openai_body inherits the same failure mode automatically.
Remaining instances
Counts below are catalog-derived projections, not observed failures. They are computed from the supported_parameters array in GET https://openrouter.ai/api/v1/models (343 model ids, 274 tools-capable), which is OpenRouter's advertisement of endpoint capability rather than tested behavior. The mechanism predicted the live google/gemini-2.5-flash 404 exactly, so the mechanism is real; the specific counts assume the catalog is accurate for all 274 and that a model's advertised set is the router's actual filter.
Catalog snapshot taken 2026-07-26.
| Params we send |
Models satisfying require_parameters |
Projected 404s |
max_tokens + tools + tool_choice (no thinking effort — today's default path) |
268 / 274 |
6 |
the above + reasoning (thinking effort set) |
190 / 274 |
84 |
drop tool_choice when un-advertised |
272 / 274 |
2 |
With-effort breakdown of the 84:
78 missing ['reasoning']
4 missing ['reasoning', 'tool_choice']
1 missing ['max_tokens']
1 missing ['max_tokens', 'reasoning']
1. reasoning — 83 of 274 tools-capable models do not advertise it. llm.rs:1549-1556 inserts reasoning: { effort: ... } whenever a thinking effort is configured, and :1563 makes has_reasoning sufficient to set require_parameters. config.rs:839 reads BUZZ_AGENT_THINKING_EFFORT for every provider, so this is one env var away from reproducing today on models like deepseek/deepseek-chat, cohere/command-r-08-2024, google/gemma-3-12b-it, or the Amazon Nova family. Note this is opt-in: parse_thinking_effort maps None | Some("") to Ok(None) (config.rs:622-624), so the default path is unaffected.
2. tool_choice — 4 tools-capable models advertise tools without tool_choice. openai_body sets tool_choice: "auto" unconditionally whenever tools are non-empty (llm.rs:591-594). The 4: amazon/nova-lite-v1, amazon/nova-micro-v1, amazon/nova-premier-v1, amazon/nova-pro-v1. This is why 6 models fail even with no effort set.
3. max_tokens — 2 tools-capable models advertise no token-limit parameter at all. openai/gpt-3.5-turbo-0613 and sakana/fugu-ultra. No spelling satisfies require_parameters for these; the parameter can only be dropped.
The cheap win
Omitting tool_choice when a model does not advertise it takes the no-effort projected failure set from 6 to 2, and tool_choice: "auto" is the OpenAI-compatible API default anyway — omitting it is semantically free. Highest fix-to-risk ratio in the set.
Resolution options
Three shapes, listed without a recommendation — each trades silent degradation against hard 404 differently, and that is a product call:
- Drop
require_parameters entirely. OpenRouter then routes to a best-effort endpoint and silently ignores unsupported parameters. No 404s ever; the cost is that a user who sets a thinking effort may get a non-reasoning response with no signal that their setting was discarded.
- Gate
require_parameters on advertised support. Fetch supported_parameters for the effective model and set the flag only when every parameter in the body is advertised. Keeps the strictness guarantee where it can be honored; adds a catalog dependency (and a cache/staleness question) to the request path.
- Condition the parameters themselves on the catalog. Omit
reasoning / tool_choice when the target does not advertise them, and keep require_parameters unconditional. Same catalog dependency as option 2, but degrades by dropping a field rather than by weakening the routing contract — and would want a user-visible warning when a requested effort is dropped.
Options 2 and 3 both need a decision on failure mode when the catalog fetch fails or the model is absent from it.
Reproduce
Requires a real OpenRouter key. On a build that includes the #1975 max_tokens fix:
BUZZ_AGENT_PROVIDER=openrouter \
OPENROUTER_API_KEY=... \
OPENROUTER_MODEL=deepseek/deepseek-chat \
BUZZ_AGENT_THINKING_EFFORT=low \
buzz-agent # drive one session/prompt -> HTTP 404 "No endpoints found..."
Same command without BUZZ_AGENT_THINKING_EFFORT is expected to succeed, which is the instance-1 boundary. For instance 2, use OPENROUTER_MODEL=amazon/nova-pro-v1 with no effort set.
Recompute the projections against the live catalog:
curl -sS https://openrouter.ai/api/v1/models \
| python3 -c 'import json,sys
d=json.load(sys.stdin)["data"]
sp=lambda m:set(m.get("supported_parameters") or [])
t=[m for m in d if "tools" in sp(m)]
for req in [{"max_tokens","tools","tool_choice"},{"max_tokens","tools","tool_choice","reasoning"},{"max_tokens","tools"}]:
f=[m["id"] for m in t if req-sp(m)]
print(sorted(req), "satisfy", len(t)-len(f), "fail", len(f))'
Not in scope of #1975
#1975 fixed the max_completion_tokens -> max_tokens translation, which was the only instance affecting the default path (221 of 274 tools-capable models, i.e. every OpenRouter user regardless of settings). Everything above is either opt-in or narrow, and resolving it changes how request bodies are constructed for every provider-adjacent path — deliberately kept out of that PR.
Summary
On the OpenRouter path we send
provider: { "require_parameters": true }whenever the request body carries tools or areasoningobject (crates/buzz-agent/src/llm.rs:1558-1566). That flag is an assertion that every parameter in our body is honored by the endpoint OpenRouter routes to. But we build the body from our own capability model — not from the target model's advertisedsupported_parameters— so any parameter we send unconditionally is a latent hard 404 for whatever slice of the catalog does not advertise it.The failure is not graceful. OpenRouter returns:
and
openrouter_postmaps any 404 toAgentError::LlmModelNotFound(llm.rs:1444-1449), which surfaces to the user asllm model not found: (<model>) 404: .... The message sends people hunting a model-name typo that does not exist — the model is present in the catalog and the key is valid.Worth noting the diagnostic already exists one branch away:
llm.rs:1487-1491producesno OpenRouter endpoint supports the requested parameters — check model, effort, and tool requirementsfor a bare 503. The 404 that OpenRouter actually returns for this condition never reaches it, because the 404 arm returns first.This was found while verifying #1975 against a live OpenRouter key. The largest instance (
max_completion_tokens, 221/274 tools-capable models) is fixed on that branch. The pattern and the remaining instances are not.The pattern
Naming the pattern matters more than the individual instances below — the next unconditional field added to
openai_bodyinherits the same failure mode automatically.Remaining instances
Counts below are catalog-derived projections, not observed failures. They are computed from the
supported_parametersarray inGET https://openrouter.ai/api/v1/models(343 model ids, 274 tools-capable), which is OpenRouter's advertisement of endpoint capability rather than tested behavior. The mechanism predicted the livegoogle/gemini-2.5-flash404 exactly, so the mechanism is real; the specific counts assume the catalog is accurate for all 274 and that a model's advertised set is the router's actual filter.Catalog snapshot taken 2026-07-26.
require_parametersmax_tokens+tools+tool_choice(no thinking effort — today's default path)reasoning(thinking effort set)tool_choicewhen un-advertisedWith-effort breakdown of the 84:
1.
reasoning— 83 of 274 tools-capable models do not advertise it.llm.rs:1549-1556insertsreasoning: { effort: ... }whenever a thinking effort is configured, and:1563makeshas_reasoningsufficient to setrequire_parameters.config.rs:839readsBUZZ_AGENT_THINKING_EFFORTfor every provider, so this is one env var away from reproducing today on models likedeepseek/deepseek-chat,cohere/command-r-08-2024,google/gemma-3-12b-it, or the Amazon Nova family. Note this is opt-in:parse_thinking_effortmapsNone | Some("")toOk(None)(config.rs:622-624), so the default path is unaffected.2.
tool_choice— 4 tools-capable models advertisetoolswithouttool_choice.openai_bodysetstool_choice: "auto"unconditionally whenever tools are non-empty (llm.rs:591-594). The 4:amazon/nova-lite-v1,amazon/nova-micro-v1,amazon/nova-premier-v1,amazon/nova-pro-v1. This is why 6 models fail even with no effort set.3.
max_tokens— 2 tools-capable models advertise no token-limit parameter at all.openai/gpt-3.5-turbo-0613andsakana/fugu-ultra. No spelling satisfiesrequire_parametersfor these; the parameter can only be dropped.The cheap win
Omitting
tool_choicewhen a model does not advertise it takes the no-effort projected failure set from 6 to 2, andtool_choice: "auto"is the OpenAI-compatible API default anyway — omitting it is semantically free. Highest fix-to-risk ratio in the set.Resolution options
Three shapes, listed without a recommendation — each trades silent degradation against hard 404 differently, and that is a product call:
require_parametersentirely. OpenRouter then routes to a best-effort endpoint and silently ignores unsupported parameters. No 404s ever; the cost is that a user who sets a thinking effort may get a non-reasoning response with no signal that their setting was discarded.require_parameterson advertised support. Fetchsupported_parametersfor the effective model and set the flag only when every parameter in the body is advertised. Keeps the strictness guarantee where it can be honored; adds a catalog dependency (and a cache/staleness question) to the request path.reasoning/tool_choicewhen the target does not advertise them, and keeprequire_parametersunconditional. Same catalog dependency as option 2, but degrades by dropping a field rather than by weakening the routing contract — and would want a user-visible warning when a requested effort is dropped.Options 2 and 3 both need a decision on failure mode when the catalog fetch fails or the model is absent from it.
Reproduce
Requires a real OpenRouter key. On a build that includes the #1975
max_tokensfix:BUZZ_AGENT_PROVIDER=openrouter \ OPENROUTER_API_KEY=... \ OPENROUTER_MODEL=deepseek/deepseek-chat \ BUZZ_AGENT_THINKING_EFFORT=low \ buzz-agent # drive one session/prompt -> HTTP 404 "No endpoints found..."Same command without
BUZZ_AGENT_THINKING_EFFORTis expected to succeed, which is the instance-1 boundary. For instance 2, useOPENROUTER_MODEL=amazon/nova-pro-v1with no effort set.Recompute the projections against the live catalog:
Not in scope of #1975
#1975 fixed the
max_completion_tokens->max_tokenstranslation, which was the only instance affecting the default path (221 of 274 tools-capable models, i.e. every OpenRouter user regardless of settings). Everything above is either opt-in or narrow, and resolving it changes how request bodies are constructed for every provider-adjacent path — deliberately kept out of that PR.