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.
The chat completions round-trip is three steps: build the request body with ChatRequestBuilder::build(), POST it to the provider’s /chat/completions endpoint, then parse the JSON payload with ChatResponseParser::parse(). The adapter handles all wire-format details — message encoding, tool serialization, structured output schemas, and provider metadata extraction — so your provider package stays focused on auth and endpoint routing.
Building the request body
ChatRequestBuilder::build() takes the model ID, provider name, a TextModelRequest, and a $stream flag, and returns a plain PHP array ready to JSON-encode and POST.
use AiSdk\OpenAICompatible\ChatRequestBuilder;
use AiSdk\Requests\TextModelRequest;
use AiSdk\Message;
$request = new TextModelRequest(
messages: [Message::user('What is the capital of France?')],
system: 'You are a concise geography assistant.',
temperature: 0.7,
maxTokens: 256,
topP: 0.9,
);
$body = ChatRequestBuilder::build(
modelId: 'gpt-4o',
providerName: 'openai',
request: $request,
stream: false,
);
The resulting array always contains these fields:
| Field | Source |
|---|
model | $modelId parameter |
messages | Converted from $request->messages + $request->system |
temperature | $request->temperature |
max_tokens | $request->maxTokens |
stream | $stream parameter |
top_p | $request->topP (omitted when null) |
tools + tool_choice | Included when $request->tools !== [] |
response_format | Included when $request->output is set |
stream_options | {include_usage: true} when $stream === true |
reasoning_effort | $request->reasoning->effort (when set) |
The raw escape hatch lets you merge provider-specific fields that the portable API doesn’t expose. Pass them under providerOptions and they are merged last, overwriting any key the builder already set:use AiSdk\Requests\TextModelRequest;
$request = new TextModelRequest(
messages: [Message::user('Repeat after me: hello')],
providerOptions: [
'openai' => ['raw' => ['temperature' => 0.1, 'seed' => 42]],
],
);
$body = ChatRequestBuilder::build('gpt-4o', 'openai', $request, false);
// $body['temperature'] === 0.1
// $body['seed'] === 42
Structured output
When $request->output is an Output instance, ChatRequestBuilder appends a response_format key to the body. Two formats are supported:
JSON schema — used when $request->output has Output::KIND_OBJECT and an associated Schema object:
use AiSdk\Outputs\Output;
use AiSdk\Schema;
$request = new TextModelRequest(
messages: [Message::user('Extract the event details.')],
output: Output::object(Schema::fromArray([
'name' => 'event',
'schema' => [
'type' => 'object',
'properties' => [
'title' => ['type' => 'string'],
'location' => ['type' => 'string'],
'date' => ['type' => 'string'],
],
'required' => ['title', 'location', 'date'],
],
])),
);
$body = ChatRequestBuilder::build('gpt-4o', 'openai', $request, false);
// $body['response_format'] === [
// 'type' => 'json_schema',
// 'json_schema' => [
// 'name' => 'event',
// 'strict' => true,
// 'schema' => [...],
// ],
// ]
JSON object — used when $request->output has Output::KIND_OBJECT but no schema:
$body['response_format'] = ['type' => 'json_object'];
Parsing the response
Pass the raw decoded JSON payload to ChatResponseParser::parse(). It returns a TextModelResponse with typed parts, usage, finish reason, and provider metadata.
use AiSdk\OpenAICompatible\ChatResponseParser;
$payload = $this->runner()->postJson($url, $body, $headers, 'openai');
$response = ChatResponseParser::parse($payload, 'openai');
Accessing response data
// Plain text content
echo $response->text();
// Tool calls (returns ToolCallPart[])
foreach ($response->toolCalls() as $call) {
echo $call->id; // e.g. 'call_abc123'
echo $call->name; // e.g. 'get_weather'
print_r($call->arguments); // decoded array, e.g. ['city' => 'Lahore']
}
// Token usage
echo $response->usage->inputTokens; // prompt tokens
echo $response->usage->outputTokens; // completion tokens
// Provider metadata (response-level fields and choice-level fields)
$meta = $response->providerMetadata['openai'];
echo $meta['id']; // e.g. 'chatcmpl_123'
echo $meta['model']; // e.g. 'gpt-4o'
echo $meta['choice_finish_reason']; // e.g. 'tool_calls'
echo $meta['system_fingerprint'];
The following example mirrors the test in ChatWireFormatTest.php and shows exactly what the parser extracts from a real provider payload:
$payload = [
'id' => 'chatcmpl_123',
'object' => 'chat.completion',
'created' => 1710000000,
'model' => 'gpt-4o',
'system_fingerprint' => 'fp_abc',
'choices' => [[
'index' => 0,
'message' => [
'content' => 'hello',
'tool_calls' => [[
'id' => 'call_1',
'function' => [
'name' => 'weather',
'arguments' => '{"city":"Lahore"}',
],
]],
],
'finish_reason' => 'tool_calls',
]],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 4],
];
$response = ChatResponseParser::parse($payload, 'openai');
echo $response->text(); // 'hello'
echo $response->toolCalls()[0]->name; // 'weather'
print_r($response->toolCalls()[0]->arguments); // ['city' => 'Lahore']
echo $response->usage->inputTokens; // 10
echo $response->providerMetadata['openai']['id']; // 'chatcmpl_123'
Reasoning support
Reasoning effort is mapped through a single, unambiguous path. Use Reasoning::effort() to pass a portable effort string:
use AiSdk\Reasoning;
$request = new TextModelRequest(
messages: [Message::user('Solve this step by step.')],
reasoning: Reasoning::effort('high'),
);
$body = ChatRequestBuilder::build('o3', 'openai', $request, false);
// $body['reasoning_effort'] === 'high'
Token budget reasoning (Reasoning::budget(N)) is not supported by the OpenAI-compatible wire format and throws an InvalidArgumentException. Use Reasoning::effort() instead, or pass provider-specific budget parameters through the raw escape hatch.
For tool calling and multi-turn conversations, see Tool Calling. For multimodal content in messages, see Multimodal Content.