Add Reasoning attribute for disabling reasoning across providers#486
Add Reasoning attribute for disabling reasoning across providers#486ChrisThompsonTLDR wants to merge 1 commit into
Conversation
Adds a `#[Reasoning(false)]` class attribute (and matching `reasoning()`
agent method) that lets an agent express "skip reasoning / thinking"
once and have each gateway translate it to the right wire-format key.
Per-provider mapping when reasoning is disabled:
| Provider | Wire format |
|------------|----------------------------------------|
| Ollama | top-level `think: false` |
| OpenAI | `reasoning: { effort: "minimal" }` |
| Anthropic | strips any `thinking` provider option |
| Gemini | `generationConfig.thinkingConfig.thinkingBudget = 0` |
| OpenRouter | `reasoning: { exclude: true }` |
Other providers (Mistral, Groq, DeepSeek, xAI, Bedrock) treat the
attribute as a graceful no-op. Existing `providerOptions()` overrides
continue to win, so the change is fully backward compatible.
The attribute mirrors the resolution rules of `#[Temperature]`,
`#[MaxSteps]` etc.: an `reasoning()` method on the agent takes
precedence over the class attribute.
|
Thanks for your pull request! Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain this package, we need to be very careful regarding the amount of code we include. The core issue is that reasoning is fundamentally tied to the model name, not the agent. An agent declaring Additionally, reasoning configuration is more nuanced than a boolean: OpenAI uses For these reasons, we think |
|
Thanks so much for the careful review and for taking the time to explain the rationale — points 2 and 3 are well-taken. The boolean genuinely doesn't capture If keeping the door cracked open is at all considered, a couple of related observations might be worth weighing: On the model-coupling concern: a similar coupling is already accepted for On the boolean limitation: the current attribute is the smallest useful step, but it doesn't preclude a richer form being added later. A future On the wire-format side: there's a related PR open at prism-php/prism#1018 that adds One operational note on Whatever direction is preferred is appreciated either way. Just wanted to surface the evidence before this closed permanently. |
Summary
Adds a
#[Reasoning(false)]class attribute (and matchingreasoning()agent method) so an agent can express "skip reasoning / thinking" once and have each gateway translate it to the right wire-format key.Why
Each provider exposes reasoning / thinking through a different field: Ollama uses
think, OpenAI usesreasoning.effort, Anthropic uses athinkingblock, Gemini usesthinkingConfig.thinkingBudget, OpenRouter usesreasoning.exclude. To disable reasoning today, an agent has to know each provider's wire format and hand-roll it viaproviderOptions(). Code that's portable across providers (an agent that can run against an OpenAI or an Ollama backend at runtime) ends up maintaining a translation table.This PR puts that table inside the SDK.
Per-provider mapping when reasoning is disabled
think: falsereasoning: { effort: "minimal" }thinkingprovider optiongenerationConfig.thinkingConfig.thinkingBudget = 0reasoning: { exclude: true }Other providers (Mistral, Groq, DeepSeek, xAI, Bedrock) treat the attribute as a graceful no-op — no error, no extra key sent.
Example
The same can be expressed via a method, mirroring how
temperature()/maxSteps()work today:The method takes priority over the attribute, matching the resolution order in
TextGenerationOptions::forAgent().Backward compatibility
Fully additive. If
#[Reasoning]is never declared and noreasoning()method exists, requests are built exactly as today. ExistingproviderOptions()keys (think,reasoning,thinkingConfig) still win — the new logic only fills in the default when the corresponding key is not already set.Tests
tests/Feature/AgentAttributeTest.php: attribute resolution, method takes priority, null when neither is settests/Feature/Providers/Ollama/RequestMappingTest.php:think: falseis sent at the top level when reasoning is disabled; key is absent otherwisetests/Feature/Providers/OpenAi/RequestMappingTest.php:reasoning.effort = minimalis sent when reasoning is disabled; key is absent otherwisevendor/bin/pest --exclude-group=integrationreports 1012 passing / 1 pre-existing skipped. (3 integration tests inAgentReasoningTestfail on the base branch as well due to a missingpdo_sqliteextension in this environment — unrelated to this change.)vendor/bin/pint --testis clean for every file changed by this PR.