Move tool-forcing and thinking-default profile flags to `ModelProfile` so they apply on every provider route
Why we make these changes
Whether a model accepts a forced tool_choice was spread over six provider-prefixed profile keys (openai_supports_tool_choice_required, grok_supports_tool_choice_required, anthropic_supports_forced_tool_choice, openai_/openrouter_supports_forced_tool_choice_with_thinking, openai_reasoning_enabled_by_default). Each adapter only read its own prefix, so a model fact didn't reach the other routes that serve it. For example, Claude Opus 5.5 and Fable 5.1 reject forcing with a 400 on the Anthropic API, but OpenRouter, Vercel, GitHub Copilot, Heroku, LiteLLM and Snowflake kept sending a forced choice (#8675). Each adapter also had its own copy of the "explicit forcing raises, inferred forcing falls back" logic, and they had drifted apart.
This moves these facts to ModelProfile, so they follow the model onto every route, and decides forcing through one shared helper. It is the groundwork for the Anthropic output-mode fix for #8772 / #8640, which follows in a separate PR on top of this one.
New public surface
ModelProfile.supports_forced_tool_choice,ModelProfile.supports_forced_tool_choice_with_thinking,ModelProfile.thinking_enabled_by_default- The six old keys still work: they are translated on merge with a
PydanticAIDeprecationWarning, including when returned from a callableprofile=. The same fix applies to the oldertool_additionsanddeferred_tools_require_tool_searchkeys, which a callableprofile=couldn't set before when the resolved profile already had a value.
User-visible behavior
- Claude Opus 5.5 / Sonnet 5.5 / Fable 5.1 / Mythos 5.1 on OpenAI-compatible routes: an inferred forced choice (for example, from an output tool) falls back to
'auto'instead of a 400. An explicittool_choice='required'or list of tools raises aUserError. - OpenRouter: "is thinking on" now also counts
openai_reasoning_effortand models that think by default (#7831). - OpenAI-compatible models that can't force:
ToolOrOutputnow falls back to'auto'like any other inferred forcing, where it used to raise. This matches Anthropic and Bedrock. - xAI profiles with forcing disabled: an explicit
'required'or list of tools now raises, like every other provider, where it used to fall back silently. No built-in Grok profile disables forcing. - The forcing error messages share one format:
tool_choice=... is not supported by model '...'. <reason and remedy>.
Verification
tests/profiles/test_legacy_keys.py: legacy key translation, including the callableprofile=formtests/profiles/test_resolution_matrix.py::test_forced_tool_choice_support_follows_the_model_on_every_routetests/models/test_openrouter.py::test_openrouter_forced_tool_choice_on_anthropic_model_that_rejects_it(fails onmain) and::test_openrouter_forced_tool_choice_follows_default_thinkingtests/models/test_tool_choice_unit.py: xAI thinking-state, explicit-raise andtool_choice=[]cases, plus the updated shared-helper cases
What changes for existing users
Only the routes and cases listed above change; custom profiles using the old keys keep working, with a deprecation warning.
Supersedes #8676, which maps the Anthropic flag onto OpenRouter only. Follow-up: Vercel's Anthropic profile lookup doesn't normalise dotted ids such as anthropic/claude-opus-5.5, which predates this PR.
Checklist
- Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
- No breaking changes in accordance with the version policy.
- Any permitted compatibility impact has a label, warning, migration, release note, and exact API-check waiver.
- PR title is fit for the release changelog.
š¤ Generated with Claude Code