Skip to content

feat: add provider-agnostic withReasoning() request method#1018

Open
ChrisThompsonTLDR wants to merge 1 commit into
prism-php:mainfrom
ChrisThompsonTLDR:feature/with-reasoning-method
Open

feat: add provider-agnostic withReasoning() request method#1018
ChrisThompsonTLDR wants to merge 1 commit into
prism-php:mainfrom
ChrisThompsonTLDR:feature/with-reasoning-method

Conversation

@ChrisThompsonTLDR
Copy link
Copy Markdown
Contributor

@ChrisThompsonTLDR ChrisThompsonTLDR commented May 2, 2026

Description

Adds a withReasoning(bool $enabled = true) fluent setter on text and structured pending requests so callers can express the semantic intent of "skip reasoning" once and have each provider translate it to the right wire-format key.

Why

Each provider that supports reasoning / thinking exposes it through a different field today:

  • Ollama uses think: true|false
  • OpenAI Responses uses reasoning: { effort: "minimal"|"low"|... }
  • Anthropic uses thinking: { type: "enabled", budget_tokens: ... } (and simply omits the block when disabled)
  • Gemini uses thinkingConfig: { thinkingBudget: 0|... }
  • OpenRouter uses reasoning: { exclude: true|... }
  • Perplexity uses reasoning_effort: "low"|...

To disable hidden chain-of-thought today, a caller has to know each provider's wire format and hand-roll it via withProviderOptions(). Code that wants to be portable across providers (e.g. an agent framework choosing between an OpenAI and an Ollama backend at runtime) has to maintain a translation table. This PR puts that table inside Prism.

What changed

  • New Prism\Prism\Concerns\HasReasoning trait with a ?bool state and withReasoning() / reasoningEnabled() accessors. Composed onto Text\PendingRequest, Text\Request, Structured\PendingRequest, Structured\Request.
  • Each provider's text, stream, and structured handler reads $request->reasoningEnabled() and emits the right key when it is false and the user has not already set the equivalent provider option:
Provider Wire format when withReasoning(false)
Ollama think: false
OpenAI reasoning: { effort: "minimal" }
Anthropic thinking block omitted
Gemini thinkingConfig: { thinkingBudget: 0 }
OpenRouter reasoning: { exclude: true }
Perplexity reasoning_effort: "low"

Other providers (Groq, Mistral, DeepSeek, xAI, etc.) treat the call as a graceful no-op — no error, no extra key sent.

Example

// Before: caller has to know the wire format
$response = Prism::text()
    ->using('ollama', 'qwen3:14b')
    ->withPrompt($prompt)
    ->withProviderOptions(['thinking' => false])
    ->asText();

// After: portable across providers
$response = Prism::text()
    ->using($provider, $model)
    ->withPrompt($prompt)
    ->withReasoning(false)
    ->asText();

Backward compatibility

Fully additive. If withReasoning() is never called the request behaves exactly as it does today. If a caller sets the equivalent provider option explicitly (e.g. withProviderOptions(['reasoning' => ['effort' => 'high']])), that explicit value still wins — withReasoning(false) only fills in the default when no provider option is set.

Tests

  • New per-provider assertions for withReasoning(false) in tests/Providers/Ollama/TextTest.php, tests/Providers/Ollama/StreamTest.php, tests/Providers/OpenAI/TextTest.php, tests/Providers/Anthropic/AnthropicTextRequestTest.php. Each covers (a) the new wire-format key is sent, (b) explicit provider options still win, (c) absence of the key when withReasoning() is not called.
  • Full suite green: vendor/bin/pest --parallel reports 1471 passing / 8 pre-existing skipped.
  • vendor/bin/pint --test and vendor/bin/phpstan analyse both clean.

Naming

Followed existing conventions: withMaxSteps, withMaxTokens, withProviderOptions already use a with* prefix on PendingRequest. withReasoning(bool $enabled = true) mirrors that shape and lets the common case read naturally as ->withReasoning(false).

Introduces a `withReasoning(bool $enabled = true)` fluent setter on the
text and structured pending requests. Calling `->withReasoning(false)`
expresses the semantic intent of "skip reasoning" and is translated to
the right wire-format key by each provider:

| Provider   | Wire format when disabled              |
|------------|----------------------------------------|
| Ollama     | `think: false`                         |
| OpenAI     | `reasoning: { effort: "minimal" }`     |
| Anthropic  | omits the `thinking` block             |
| Gemini     | `thinkingConfig: { thinkingBudget: 0 }`|
| OpenRouter | `reasoning: { exclude: true }`         |
| Perplexity | `reasoning_effort: "low"`              |

Providers that do not support disabling reasoning treat the call as a
graceful no-op. Callers that have set the equivalent key explicitly via
`withProviderOptions()` continue to win, so the change is fully
backward compatible.

The new state lives in a `HasReasoning` concern that mirrors the shape
of `HasProviderOptions` and is composed onto both `PendingRequest` and
`Request` for text and structured generation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant