Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/Concerns/HasReasoning.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Prism\Prism\Concerns;

trait HasReasoning
{
protected ?bool $reasoningEnabled = null;

/**
* Toggle reasoning / thinking output on or off in a provider-agnostic way.
*
* `withReasoning(false)` instructs the provider to skip reasoning when it
* supports doing so (e.g. Ollama `think: false`, OpenAI `reasoning.effort
* = minimal`). Providers that cannot disable reasoning treat the call as a
* graceful no-op.
*
* Calling `withReasoning(true)` is reserved for symmetry; today reasoning
* is enabled per-provider via existing options (e.g. Anthropic's
* `thinking.enabled`). This method does not override those settings.
*
* Not calling `withReasoning()` preserves prior behavior: providers honor
* whatever the user sets via `withProviderOptions()`.
*/
public function withReasoning(bool $enabled = true): self
{
$this->reasoningEnabled = $enabled;

return $this;
}

public function reasoningEnabled(): ?bool
{
return $this->reasoningEnabled;
}
}
3 changes: 2 additions & 1 deletion src/Providers/Anthropic/Handlers/Structured.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ public static function buildHttpRequestPayload(PrismRequest $request): array
'model' => $request->model(),
'messages' => MessageMap::map($request->messages(), $request->providerOptions()),
'system' => MessageMap::mapSystemMessages($request->systemPrompts()) ?: null,
'thinking' => $request->providerOptions('thinking.enabled') === true
'thinking' => $request->reasoningEnabled() !== false
&& $request->providerOptions('thinking.enabled') === true
? [
'type' => 'enabled',
'budget_tokens' => is_int($request->providerOptions('thinking.budgetTokens'))
Expand Down
3 changes: 2 additions & 1 deletion src/Providers/Anthropic/Handlers/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ public static function buildHttpRequestPayload(PrismRequest $request): array
'model' => $request->model(),
'system' => MessageMap::mapSystemMessages($request->systemPrompts()) ?: null,
'messages' => MessageMap::map($request->messages(), $request->providerOptions()),
'thinking' => $request->providerOptions('thinking.enabled') === true
'thinking' => $request->reasoningEnabled() !== false
&& $request->providerOptions('thinking.enabled') === true
? [
'type' => 'enabled',
'budget_tokens' => is_int($request->providerOptions('thinking.budgetTokens'))
Expand Down
4 changes: 4 additions & 0 deletions src/Providers/Gemini/Handlers/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,10 @@ protected function sendRequest(Request $request): Response
];
}

if ($request->reasoningEnabled() === false && $thinkingConfig === null) {
$thinkingConfig = ['thinkingBudget' => 0];
}

/** @var Response $response */
$response = $this->client
->withOptions(['stream' => true])
Expand Down
4 changes: 4 additions & 0 deletions src/Providers/Gemini/Handlers/Structured.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ public function sendRequest(Request $request): array
]);
}

if ($request->reasoningEnabled() === false && $thinkingConfig === null) {
$thinkingConfig = ['thinkingBudget' => 0];
}

/** @var Response $response */
$response = $this->client->post(
"{$request->model()}:generateContent",
Expand Down
4 changes: 4 additions & 0 deletions src/Providers/Gemini/Handlers/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ protected function sendRequest(Request $request): ClientResponse
]);
}

if ($request->reasoningEnabled() === false && $thinkingConfig === null) {
$thinkingConfig = ['thinkingBudget' => 0];
}

$generationConfig = Arr::whereNotNull([
'temperature' => $request->temperature(),
'topP' => $request->topP(),
Expand Down
4 changes: 3 additions & 1 deletion src/Providers/Ollama/Handlers/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,9 @@ protected function sendRequest(Request $request): Response
'tools' => ToolMap::map($request->tools()),
'stream' => true,
...Arr::whereNotNull([
'think' => $request->providerOptions('thinking'),
'think' => $request->providerOptions('thinking') ?? (
$request->reasoningEnabled() === false ? false : null
),
'keep_alive' => $request->providerOptions('keep_alive'),
]),
'options' => Arr::whereNotNull(array_merge([
Expand Down
3 changes: 3 additions & 0 deletions src/Providers/Ollama/Handlers/Structured.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ protected function sendRequest(Request $request): array
'format' => $request->schema()->toArray(),
'stream' => false,
...Arr::whereNotNull([
'think' => $request->providerOptions('thinking') ?? (
$request->reasoningEnabled() === false ? false : null
),
'keep_alive' => $request->providerOptions('keep_alive'),
]),
'options' => Arr::whereNotNull(array_merge([
Expand Down
4 changes: 3 additions & 1 deletion src/Providers/Ollama/Handlers/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ protected function sendRequest(Request $request): array
'tools' => ToolMap::map($request->tools()),
'stream' => false,
...Arr::whereNotNull([
'think' => $request->providerOptions('thinking'),
'think' => $request->providerOptions('thinking') ?? (
$request->reasoningEnabled() === false ? false : null
),
'keep_alive' => $request->providerOptions('keep_alive'),
]),
'options' => Arr::whereNotNull(array_merge([
Expand Down
4 changes: 3 additions & 1 deletion src/Providers/OpenAI/Handlers/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,9 @@ protected function sendRequest(Request $request): Response
'verbosity' => $request->providerOptions('text_verbosity'),
] : null,
'truncation' => $request->providerOptions('truncation'),
'reasoning' => $request->providerOptions('reasoning'),
'reasoning' => $request->providerOptions('reasoning') ?? (
$request->reasoningEnabled() === false ? ['effort' => 'minimal'] : null
),
]))
);

Expand Down
4 changes: 3 additions & 1 deletion src/Providers/OpenAI/Handlers/Structured.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,9 @@ protected function sendRequest(Request $request, array $responseFormat): ClientR
'previous_response_id' => $request->providerOptions('previous_response_id'),
'service_tier' => $request->providerOptions('service_tier'),
'truncation' => $request->providerOptions('truncation'),
'reasoning' => $request->providerOptions('reasoning'),
'reasoning' => $request->providerOptions('reasoning') ?? (
$request->reasoningEnabled() === false ? ['effort' => 'minimal'] : null
),
'store' => $request->providerOptions('store'),
'text' => [
'format' => $responseFormat,
Expand Down
4 changes: 3 additions & 1 deletion src/Providers/OpenAI/Handlers/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ protected function sendRequest(Request $request): ClientResponse
'verbosity' => $request->providerOptions('text_verbosity'),
] : null,
'truncation' => $request->providerOptions('truncation'),
'reasoning' => $request->providerOptions('reasoning'),
'reasoning' => $request->providerOptions('reasoning') ?? (
$request->reasoningEnabled() === false ? ['effort' => 'minimal'] : null
),
'store' => $request->providerOptions('store'),
]))
);
Expand Down
4 changes: 4 additions & 0 deletions src/Providers/OpenRouter/Concerns/BuildsRequestOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ protected function buildRequestOptions(TextRequest|StructuredRequest $request, a
'tool_choice' => ToolChoiceMap::map($request->toolChoice()),
]));

if ($request->reasoningEnabled() === false && ! isset($options['reasoning'])) {
$options['reasoning'] = ['exclude' => true];
}

return Arr::whereNotNull(array_merge($options, $additional));
}
}
4 changes: 3 additions & 1 deletion src/Providers/Perplexity/Concerns/HandlesHttpRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ protected function buildHttpRequestPayload(TextRequest|StructuredRequest $reques
'temperature' => $request->temperature(),
'top_p' => $request->topP(),
'top_k' => $request->providerOptions('top_k'),
'reasoning_effort' => $request->providerOptions('reasoning_effort'),
'reasoning_effort' => $request->providerOptions('reasoning_effort') ?? (
$request->reasoningEnabled() === false ? 'low' : null
),
'web_search_options' => $request->providerOptions('web_search_options'),
'search_mode' => $request->providerOptions('search_mode'),
'language_preference' => $request->providerOptions('language_preference'),
Expand Down
3 changes: 3 additions & 0 deletions src/Structured/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Prism\Prism\Concerns\HasPrompts;
use Prism\Prism\Concerns\HasProviderOptions;
use Prism\Prism\Concerns\HasProviderTools;
use Prism\Prism\Concerns\HasReasoning;
use Prism\Prism\Concerns\HasSchema;
use Prism\Prism\Concerns\HasTools;
use Prism\Prism\Contracts\Schema;
Expand All @@ -33,6 +34,7 @@ class PendingRequest
use HasPrompts;
use HasProviderOptions;
use HasProviderTools;
use HasReasoning;
use HasSchema;
use HasTools;

Expand Down Expand Up @@ -89,6 +91,7 @@ public function toRequest(): Request
maxSteps: $this->maxSteps,
providerOptions: $this->providerOptions,
providerTools: $this->providerTools,
reasoningEnabled: $this->reasoningEnabled,
);
}
}
5 changes: 4 additions & 1 deletion src/Structured/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Closure;
use Prism\Prism\Concerns\ChecksSelf;
use Prism\Prism\Concerns\HasProviderOptions;
use Prism\Prism\Concerns\HasReasoning;
use Prism\Prism\Contracts\Message;
use Prism\Prism\Contracts\PrismRequest;
use Prism\Prism\Contracts\Schema;
Expand All @@ -18,7 +19,7 @@

class Request implements PrismRequest
{
use ChecksSelf, HasProviderOptions;
use ChecksSelf, HasProviderOptions, HasReasoning;

/**
* @param SystemMessage[] $systemPrompts
Expand Down Expand Up @@ -47,8 +48,10 @@ public function __construct(
protected int $maxSteps,
array $providerOptions = [],
protected array $providerTools = [],
?bool $reasoningEnabled = null,
) {
$this->providerOptions = $providerOptions;
$this->reasoningEnabled = $reasoningEnabled;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/Text/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Prism\Prism\Concerns\HasPrompts;
use Prism\Prism\Concerns\HasProviderOptions;
use Prism\Prism\Concerns\HasProviderTools;
use Prism\Prism\Concerns\HasReasoning;
use Prism\Prism\Concerns\HasTools;
use Prism\Prism\Exceptions\PrismException;
use Prism\Prism\Streaming\Adapters\BroadcastAdapter;
Expand All @@ -38,6 +39,7 @@ class PendingRequest
use HasPrompts;
use HasProviderOptions;
use HasProviderTools;
use HasReasoning;
use HasTools;

/**
Expand Down Expand Up @@ -146,6 +148,7 @@ public function toRequest(): Request
toolChoice: $this->toolChoice,
providerOptions: $this->providerOptions,
providerTools: $this->providerTools,
reasoningEnabled: $this->reasoningEnabled,
);
}
}
5 changes: 4 additions & 1 deletion src/Text/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Closure;
use Prism\Prism\Concerns\ChecksSelf;
use Prism\Prism\Concerns\HasProviderOptions;
use Prism\Prism\Concerns\HasReasoning;
use Prism\Prism\Contracts\Message;
use Prism\Prism\Contracts\PrismRequest;
use Prism\Prism\Enums\ToolChoice;
Expand All @@ -16,7 +17,7 @@

class Request implements PrismRequest
{
use ChecksSelf, HasProviderOptions;
use ChecksSelf, HasProviderOptions, HasReasoning;

/**
* @param SystemMessage[] $systemPrompts
Expand All @@ -43,8 +44,10 @@ public function __construct(
protected string|ToolChoice|null $toolChoice,
array $providerOptions = [],
protected array $providerTools = [],
?bool $reasoningEnabled = null,
) {
$this->providerOptions = $providerOptions;
$this->reasoningEnabled = $reasoningEnabled;
}

public function toolChoice(): string|ToolChoice|null
Expand Down
37 changes: 37 additions & 0 deletions tests/Providers/Anthropic/AnthropicTextRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,43 @@
});
});

it('omits thinking when withReasoning(false) is used even with thinking.enabled', function (): void {
FixtureResponse::fakeResponseSequence('v1/messages', 'anthropic/generate-text-with-a-prompt');

Prism::text()
->using(Provider::Anthropic, 'claude-3-5-haiku-latest')
->withMessages([new UserMessage('Test')])
->withProviderOptions(['thinking' => ['enabled' => true]])
->withReasoning(false)
->asText();

Http::assertSent(function (Request $request): bool {
$payload = $request->data();

expect($payload)->not->toHaveKey('thinking');

return true;
});
});

it('does not include thinking when withReasoning(false) on a non-thinking model', function (): void {
FixtureResponse::fakeResponseSequence('v1/messages', 'anthropic/generate-text-with-a-prompt');

Prism::text()
->using(Provider::Anthropic, 'claude-3-5-haiku-latest')
->withMessages([new UserMessage('Test')])
->withReasoning(false)
->asText();

Http::assertSent(function (Request $request): bool {
$payload = $request->data();

expect($payload)->not->toHaveKey('thinking');

return true;
});
});

it('sends correct thinking mode with default budget tokens', function (): void {
FixtureResponse::fakeResponseSequence('v1/messages', 'anthropic/generate-text-with-a-prompt');

Expand Down
22 changes: 22 additions & 0 deletions tests/Providers/Ollama/StreamTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,28 @@
});
});

it('sends think:false on stream when withReasoning(false) is used', function (): void {
FixtureResponse::fakeStreamResponses('api/chat', 'ollama/stream-without-thinking');

$response = Prism::text()
->using('ollama', 'gpt-oss')
->withPrompt('Test prompt')
->withReasoning(false)
->asStream();

foreach ($response as $chunk) {
break;
}

Http::assertSent(function (Request $request): true {
$body = $request->data();
expect($body)->toHaveKey('think');
expect($body['think'])->toBe(false);

return true;
});
});

it('includes keep_alive parameter when provided for streaming', function (): void {
FixtureResponse::fakeStreamResponses('api/chat', 'ollama/stream-without-thinking');

Expand Down
37 changes: 37 additions & 0 deletions tests/Providers/Ollama/TextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,43 @@
return true;
});
});

it('sends think:false when withReasoning(false) is used', function (): void {
FixtureResponse::fakeResponseSequence('api/chat', 'ollama/text-without-thinking');

Prism::text()
->using('ollama', 'gpt-oss')
->withPrompt('Test prompt')
->withReasoning(false)
->asText();

Http::assertSent(function (Request $request): true {
$body = $request->data();
expect($body)->toHaveKey('think');
expect($body['think'])->toBe(false);

return true;
});
});

it('honors explicit thinking provider option over withReasoning(false)', function (): void {
FixtureResponse::fakeResponseSequence('api/chat', 'ollama/text-with-thinking-enabled');

Prism::text()
->using('ollama', 'gpt-oss')
->withPrompt('Test prompt')
->withProviderOptions(['thinking' => true])
->withReasoning(false)
->asText();

Http::assertSent(function (Request $request): true {
$body = $request->data();
expect($body)->toHaveKey('think');
expect($body['think'])->toBe(true);

return true;
});
});
});

describe('Keep alive parameter', function (): void {
Expand Down
Loading
Loading