Skip to content

Commit a9173fb

Browse files
authored
Merge pull request #62 from moe-mizrak/feat/add-support-to-reasoning-parameter
feat: new reasoning parameter is added to ChatData
2 parents e7d962a + 52218e0 commit a9173fb

File tree

7 files changed

+251
-4
lines changed

7 files changed

+251
-4
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ The [`ChatData`](src/DTO/ChatData.php) class is used to **encapsulate the data**
109109
- **usage** (bool|null): A boolean indicating whether to include usage information in the response. Default is `false` because enabling usage accounting will add a few hundred milliseconds to the response as the API calculates token counts and costs.
110110
- **stop** (array|string|null): A value specifying the stop sequence for the chat generation.
111111
- **stream** (bool|null): A boolean indicating whether streaming should be enabled or not.
112-
- **include_reasoning** (bool|null): Whether to return the model's reasoning.
112+
- **include_reasoning** (bool|null): Whether to return the model's reasoning (Note: this parameter is **deprecated**, use `reasoning` parameter instead. For backward compatibility, package still supports the `include_reasoning` parameter)
113+
- **reasoning** (ReasoningData|null): An instance of the [`ReasoningData`](src/DTO/ReasoningData.php) class for reasoning configuration. It provides a transparent look into the reasoning steps taken by a model.
113114

114115
#### LLM Parameters
115116

@@ -172,7 +173,10 @@ $chatData = new ChatData(
172173
usage: true,
173174
stop: ['stop_token'],
174175
stream: true,
175-
include_reasoning: true,
176+
reasoning: new ReasoningData(
177+
effort: EffortType::HIGH,
178+
exclude: false,
179+
),
176180
max_tokens: 1024,
177181
temperature: 0.7,
178182
top_p: 0.9,

src/DTO/ChatData.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,29 @@ public function __construct(
142142

143143
/**
144144
* Enable think tokens.
145-
* Default: false
145+
* Note: This parameter is the legacy parameter and will be removed in the future.
146146
*
147147
* @var bool|null
148+
* @deprecated Use '$reasoning' parameter instead (it is backward compatible with the old parameter).
148149
*/
149150
public ?bool $include_reasoning = false,
151+
152+
/**
153+
* For models that support it, the OpenRouter API can return Reasoning Tokens, also known as thinking tokens.
154+
* See: https://openrouter.ai/docs/use-cases/reasoning-tokens
155+
*
156+
* @var ReasoningData|null
157+
*/
158+
public ?ReasoningData $reasoning = null,
150159
) {
151160
$this->validateXorFields($this->messages, $this->prompt);
152161
$this->validateXorFields($this->model, $this->models);
153162

163+
// Legacy mapping: only if no explicit reasoning provided
164+
if ($this->reasoning === null && $this->include_reasoning !== null) {
165+
$this->reasoning = new ReasoningData(exclude: ! $this->include_reasoning);
166+
}
167+
154168
parent::__construct(...func_get_args());
155169
}
156170

@@ -220,7 +234,7 @@ public function convertToArray(): array
220234
'models' => $this->models,
221235
'route' => $this->route,
222236
'provider' => $this->provider?->convertToArray(),
223-
'include_reasoning' => $this->include_reasoning,
237+
'reasoning' => $this->reasoning?->convertToArray(),
224238
],
225239
fn($value) => $value !== null
226240
);

src/DTO/DeltaData.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ public function __construct(
3131
*/
3232
public ?string $role = null,
3333

34+
/**
35+
* @var string|null
36+
*/
37+
public ?string $refusal = null,
38+
39+
/**
40+
* @var string|null
41+
*/
42+
public ?string $reasoning = null,
43+
3444
/**
3545
* Calling tools e.g. function
3646
*

src/DTO/MessageData.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ public function __construct(
3131
*/
3232
public ?string $role = null,
3333

34+
/**
35+
* @var string|null
36+
*/
37+
public ?string $refusal = null,
38+
39+
/**
40+
* Reasoning for the message.
41+
*
42+
* @var string|null
43+
*/
44+
public ?string $reasoning = null,
45+
3446
/**
3547
* Calling tools e.g. function
3648
*

src/DTO/ReasoningData.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MoeMizrak\LaravelOpenrouter\DTO;
6+
7+
use MoeMizrak\LaravelOpenrouter\Rules\AllowedValues;
8+
use MoeMizrak\LaravelOpenrouter\Types\EffortType;
9+
10+
/**
11+
* ReasoningData is the DTO for the reasoning parameters of the API call.
12+
* For more info: https://openrouter.ai/docs/use-cases/reasoning-tokens
13+
*
14+
* Class ReasoningData
15+
*
16+
* @package MoeMizrak\LaravelOpenrouter\DTO
17+
*/
18+
final class ReasoningData extends DataTransferObject
19+
{
20+
/**
21+
* @inheritDoc
22+
*/
23+
public function __construct(
24+
/**
25+
* OpenAI-style reasoning effort setting
26+
*
27+
* @var string|null
28+
*/
29+
#[AllowedValues([EffortType::HIGH, EffortType::MEDIUM, EffortType::LOW])]
30+
public ?string $effort = null,
31+
32+
/**
33+
* Non-OpenAI-style reasoning effort setting.
34+
* Note: Cannot be used simultaneously with effort.
35+
*
36+
* @var int|null
37+
*/
38+
public ?int $max_tokens = null,
39+
40+
/**
41+
* Whether to exclude reasoning from the response
42+
*
43+
* @var bool|null
44+
*/
45+
public ?bool $exclude = false,
46+
47+
/**
48+
* Enable reasoning with the default parameters.
49+
* Default: inferred from `effort` or `max_tokens`
50+
*
51+
* @var bool|null
52+
*/
53+
public ?bool $enabled = null,
54+
) {
55+
parent::__construct(...func_get_args());
56+
}
57+
58+
/**
59+
* @return array
60+
*/
61+
public function convertToArray(): array
62+
{
63+
return array_filter(
64+
[
65+
'effort' => $this->effort,
66+
'max_tokens' => $this->max_tokens,
67+
'exclude' => $this->exclude,
68+
'enabled' => $this->enabled,
69+
],
70+
fn($value) => $value !== null
71+
);
72+
}
73+
}

src/Types/EffortType.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MoeMizrak\LaravelOpenrouter\Types;
6+
7+
/**
8+
* Reasoning effort level. Currently supported by the OpenAI o-series and Grok models.
9+
* See: https://openrouter.ai/docs/use-cases/reasoning-tokens
10+
*
11+
* Class EffortType
12+
* @package MoeMizrak\LaravelOpenrouter\Types
13+
*/
14+
final readonly class EffortType
15+
{
16+
/**
17+
* Allocates a large portion of tokens for reasoning
18+
*
19+
* @var string
20+
*/
21+
const HIGH = 'high';
22+
23+
/**
24+
* Allocates a moderate portion of tokens (approximately 50% of max_tokens)
25+
*
26+
* @var string
27+
*/
28+
const MEDIUM = 'medium';
29+
30+
/**
31+
* Allocates a smaller portion of tokens (approximately 20% of max_tokens)
32+
*
33+
* @var string
34+
*/
35+
const LOW = 'low';
36+
}

tests/OpenRouterAPITest.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
use MoeMizrak\LaravelOpenrouter\DTO\LimitResponseData;
1414
use MoeMizrak\LaravelOpenrouter\DTO\MessageData;
1515
use MoeMizrak\LaravelOpenrouter\DTO\ProviderPreferencesData;
16+
use MoeMizrak\LaravelOpenrouter\DTO\ReasoningData;
1617
use MoeMizrak\LaravelOpenrouter\DTO\ResponseData;
1718
use MoeMizrak\LaravelOpenrouter\DTO\ResponseFormatData;
1819
use MoeMizrak\LaravelOpenrouter\DTO\TextContentData;
1920
use MoeMizrak\LaravelOpenrouter\Exceptions\OpenRouterValidationException;
2021
use MoeMizrak\LaravelOpenrouter\Facades\LaravelOpenRouter;
2122
use MoeMizrak\LaravelOpenrouter\OpenRouterRequest;
2223
use MoeMizrak\LaravelOpenrouter\Types\DataCollectionType;
24+
use MoeMizrak\LaravelOpenrouter\Types\EffortType;
2325
use MoeMizrak\LaravelOpenrouter\Types\RoleType;
2426
use MoeMizrak\LaravelOpenrouter\Types\RouteType;
2527
use PHPUnit\Framework\Attributes\Test;
@@ -80,6 +82,37 @@ private function mockBasicBody(): array
8082
];
8183
}
8284

85+
/**
86+
* @return array
87+
*/
88+
private function mockReasoning(): array
89+
{
90+
return [
91+
'id' => 'gen-QcWgjEtiEDNHgomV2jjoQpCZlkRZ',
92+
'provider' => 'HuggingFace',
93+
'model' => $this->model,
94+
'object' => 'chat.completion',
95+
'created' => 1718888436,
96+
'choices' => [
97+
[
98+
'index' => 0,
99+
'message' => [
100+
'role' => RoleType::ASSISTANT,
101+
'content' => 'Some random content',
102+
'reasoning' => 'The reasoning behind the answer is...',
103+
],
104+
'finish_reason' => 'stop',
105+
],
106+
],
107+
'usage' => [
108+
'prompt_tokens' => 23,
109+
'completion_tokens' => 100,
110+
'total_tokens' => 123,
111+
'cost' => 0.00000114,
112+
],
113+
];
114+
}
115+
83116
/**
84117
* @return array[]
85118
*/
@@ -190,6 +223,71 @@ public function it_makes_a_basic_chat_completion_open_route_api_request()
190223
$this->assertNotNull(Arr::get($response->choices[0], 'message.content'));
191224
}
192225

226+
#[Test]
227+
public function it_makes_a_basic_chat_completion_open_route_api_request_with_reasoning_param()
228+
{
229+
/* SETUP */
230+
$chatData = new ChatData(
231+
messages: [
232+
$this->messageData,
233+
],
234+
model: $this->model,
235+
max_tokens: $this->maxTokens,
236+
reasoning: new ReasoningData(
237+
effort: EffortType::HIGH,
238+
exclude: false, // Reasoning should not be excluded
239+
),
240+
);
241+
$this->mockOpenRouter($this->mockReasoning());
242+
243+
/* EXECUTE */
244+
$response = $this->api->chatRequest($chatData);
245+
/* ASSERT */
246+
$this->generalTestAssertions($response);
247+
$this->assertNotNull(Arr::get($response->choices[0], 'message.reasoning'));
248+
}
249+
250+
#[Test]
251+
public function it_tests_chat_data_with_legacy_include_reasoning_param_if_mapping_to_reasoning()
252+
{
253+
/* SETUP */
254+
// Legacy parameter `include_reasoning` is set to true, so it should be mapped to reasoning
255+
$firstChatData = new ChatData(
256+
messages: [
257+
$this->messageData,
258+
],
259+
model: $this->model,
260+
max_tokens: $this->maxTokens,
261+
include_reasoning: true, // Legacy parameter
262+
);
263+
// neither include_reasoning nor reasoning is set, so it should not be mapped to reasoning
264+
$secondChatData = new ChatData(
265+
messages: [
266+
$this->messageData,
267+
],
268+
model: $this->model,
269+
max_tokens: $this->maxTokens,
270+
);
271+
// reasoning is set, so it should ignore legacy parameter
272+
$thirdChatData = new ChatData(
273+
messages: [
274+
$this->messageData,
275+
],
276+
model: $this->model,
277+
max_tokens: $this->maxTokens,
278+
include_reasoning: false,
279+
reasoning: new ReasoningData(
280+
effort: EffortType::HIGH,
281+
exclude: false,
282+
),
283+
);
284+
285+
/* ASSERT */
286+
$this->assertFalse($firstChatData->reasoning->exclude);
287+
$this->assertTrue($secondChatData->reasoning->exclude);
288+
$this->assertFalse($thirdChatData->reasoning->exclude);
289+
}
290+
193291
#[Test]
194292
public function it_makes_a_basic_chat_completion_open_route_api_request_with_historical_data()
195293
{

0 commit comments

Comments
 (0)