Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/phpaisdk/openai-compatible/llms.txt

Use this file to discover all available pages before exploring further.

OpenAI’s o-series models (and compatible providers such as Groq’s deepseek-r1 variants) expose a reasoning capability that can be influenced at request time. The aisdk/openai-compatible wire adapter supports this through the portable Reasoning value object from aisdk/core. There is exactly one supported path: pass a normalized effort level and the builder emits the reasoning_effort field. Any other configuration either has no effect or throws immediately so that misconfigured requests fail fast rather than silently degrading.

The Single Resolution Path

ChatRequestBuilder::build() checks for reasoning configuration after all other body fields have been assembled. The logic is intentionally narrow:
// From src/ChatRequestBuilder.php

if ($request->reasoning?->budgetTokens !== null) {
    throw new InvalidArgumentException(
        'OpenAI-compatible reasoning does not accept portable token budgets. '
        . 'Use Reasoning::effort(...) or provider raw options.'
    );
}

if ($request->reasoning?->effort !== null) {
    $body['reasoning_effort'] = $request->reasoning->effort;
}
The table below shows every possible input and its outcome:
Reasoning configurationResult
null (not set)No reasoning_effort field in body
Reasoning::effort('low')"reasoning_effort": "low"
Reasoning::effort('medium')"reasoning_effort": "medium"
Reasoning::effort('high')"reasoning_effort": "high"
Reasoning::budget(N)Throws InvalidArgumentException immediately

Using Reasoning Effort

Pass Reasoning::effort() with a level string when constructing your TextModelRequest. The builder handles the rest:
use AiSdk\Reasoning;
use AiSdk\Message;
use AiSdk\Requests\TextModelRequest;
use AiSdk\OpenAICompatible\ChatRequestBuilder;

$request = new TextModelRequest(
    messages: [Message::user('Solve this step by step: 17 × 24')],
    reasoning: Reasoning::effort('high'),
);

$body = ChatRequestBuilder::build('o3', 'openai', $request, stream: false);

// $body['reasoning_effort'] === 'high'
Valid effort strings are 'low', 'medium', and 'high'. The adapter forwards whatever string you pass directly to the provider — no validation or normalization is applied beyond the field name mapping.

What Throws an Exception

Passing a token budget instead of an effort level throws AiSdk\Exceptions\InvalidArgumentException before any HTTP call is made:
use AiSdk\Reasoning;
use AiSdk\Message;
use AiSdk\Requests\TextModelRequest;
use AiSdk\OpenAICompatible\ChatRequestBuilder;

$request = new TextModelRequest(
    messages: [Message::user('Solve this step by step: 17 × 24')],
    reasoning: Reasoning::budget(4096), // ← will throw
);

// Throws: InvalidArgumentException
// "OpenAI-compatible reasoning does not accept portable token budgets.
//  Use Reasoning::effort(...) or provider raw options."
$body = ChatRequestBuilder::build('o3', 'openai', $request, stream: false);

Why token budgets are rejected

The OpenAI-compatible wire format has no field for a portable token budget. Silently dropping the budget would mean a caller who explicitly configured reasoning depth gets a request body that ignores their intent. Throwing immediately surfaces the misconfiguration at build time rather than producing a subtly wrong response. If a specific provider you are targeting does accept a token budget under a custom field name, you can inject it via the raw escape hatch described on the Provider Options page.

Reasoning Tokens in Usage

When a reasoning model returns token counts, ChatUsage::fromArray() surfaces them in the Usage object:
// Provider response payload (abridged):
// "usage": {
//   "prompt_tokens": 25,
//   "completion_tokens": 312,
//   "completion_tokens_details": { "reasoning_tokens": 256 }
// }

$response->usage->inputTokens;     // 25
$response->usage->outputTokens;    // 312
$response->usage->reasoningTokens; // 256
The reasoning_tokens detail is read from completion_tokens_details.reasoning_tokens in the raw payload. If the provider does not return that detail, reasoningTokens is null.

Reasoning Deltas in Streams

When streaming, ChatStreamParser emits a ReasoningDeltaPart whenever a chunk contains a non-empty reasoning_content or reasoning key inside the choice delta. Some providers use reasoning_content, others use reasoning — the parser checks both and uses whichever is present:
// From src/ChatStreamParser.php
$reasoning = $delta['reasoning_content'] ?? $delta['reasoning'] ?? null;
if (is_string($reasoning) && $reasoning !== '') {
    yield new ReasoningDeltaPart($reasoning);
}
StreamState::record() accumulates all ReasoningDeltaPart fragments automatically.
If you need to inject a provider-specific reasoning field that is not reasoning_effort — for example, a custom token budget parameter accepted by one provider — use the raw escape hatch via providerOptions(). See Provider Options for details.

Build docs developers (and LLMs) love