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.

The PHP AI SDK is built in three distinct layers. The innermost layer, aisdk/core, defines portable, provider-agnostic contracts — request and response value objects, message types, tool definitions, streaming parts, and more. The middle layer, aisdk/openai-compatible, translates those portable contracts into the OpenAI-compatible wire format and back again. The outermost layer is individual provider packages (Groq, xAI, OpenRouter, and so on) that bring authentication, a base URL, a model catalog, and any provider-specific quirks. Provider packages depend on both aisdk/core and aisdk/openai-compatible, but the wire adapter itself knows nothing about any specific provider.

The Three Layers

1

Core Contracts — aisdk/core

Defines the portable types that flow across the entire stack without any provider dependency:
  • Request typesTextModelRequest, ImageRequest
  • Response typesTextModelResponse, ImageResponse
  • Message & contentMessage, Content, ContentSource, InputEncoding
  • ToolingTool, ToolChoice, ToolCall
  • Stream partsTextDeltaPart, ReasoningDeltaPart, ToolCallStartPart, ToolCallDeltaPart, FinishPart, ProviderMetadataPart
  • SupportFinishReason, Usage, Reasoning
Nothing in aisdk/core imports an HTTP client or knows about JSON field names.
2

Wire Adapter — aisdk/openai-compatible

Serializes aisdk/core contracts to OpenAI-compatible JSON request bodies and deserializes provider payloads back to aisdk/core response types. This is the only layer that knows about field names like messages, reasoning_effort, stream_options, or b64_json.The adapter is stateless; every entry point is a static method on a final class.
3

Provider Package

Wraps the wire adapter with everything that is truly provider-specific: HTTP transport and authentication headers, the base URL, the public model catalog, and any post-build adaptations (e.g., structured-output downgrades for older models). Provider packages call ChatRequestBuilder::build(), send the returned array as JSON, and hand the raw payload to ChatResponseParser::parse() or ChatStreamParser::parse().

What This Package Owns vs. Does Not Own

Owned by aisdk/openai-compatible

  • Request body building — ChatRequestBuilder
  • Chat response parsing — ChatResponseParser
  • SSE stream parsing — ChatStreamParser
  • Image request body building — ImageRequestBuilder
  • Image response parsing — ImageResponseParser
  • Message conversion — ChatMessageConverter
  • Tool & tool-choice conversion — ChatToolConverter
  • Usage normalization — ChatUsage
  • Finish-reason mapping — MapsFinishReason

NOT owned by aisdk/openai-compatible

  • Provider authentication and API keys
  • HTTP transport (sending the request)
  • Base URLs and endpoint paths
  • Model catalogs and pricing
  • Provider-specific quirks or fallback behavior
  • Streaming SSE infrastructure (reading the event stream)

Chat Completion Data Flow

The following shows what happens during a standard (non-streaming) chat call. From the caller’s perspective the call is fully synchronous; no async framework is required.
use AiSdk\OpenAICompatible\ChatRequestBuilder;
use AiSdk\OpenAICompatible\ChatResponseParser;

// 1. Translate the portable request into a wire-format array.
$body = ChatRequestBuilder::build($modelId, $providerName, $request, stream: false);

// 2. The provider package sends the array as JSON (auth headers added here).
$payload = $this->runner()->postJson($url, $body, $headers, $providerName);

// 3. Parse the raw provider payload into a typed TextModelResponse.
$response = ChatResponseParser::parse($payload, $providerName);
1

ChatRequestBuilder::build()

Converts a TextModelRequest into an array<string, mixed> suitable for json_encode(). It delegates message serialization to ChatMessageConverter, tool serialization to ChatToolConverter, resolves reasoning_effort, applies structured-output response_format, and merges any raw provider-option overrides last.
2

HTTP POST

The provider package owns this step. It encodes the array as JSON, attaches authentication headers, and POSTs to the provider’s /chat/completions endpoint. The wire adapter is not involved.
3

ChatResponseParser::parse()

Receives the decoded JSON payload as an array and returns a TextModelResponse containing typed TextPart and ToolCallPart objects, a normalized Usage (via ChatUsage::fromArray()), a mapped FinishReason, and provider metadata keyed by provider name.

Streaming Data Flow

When stream: true is passed, the wire adapter emits a PHP Generator of typed StreamPart objects. The caller accumulates them with StreamState.
use AiSdk\OpenAICompatible\ChatRequestBuilder;
use AiSdk\OpenAICompatible\ChatStreamParser;
use AiSdk\Streaming\StreamState;

// 1. Build the body with stream: true.
$body = ChatRequestBuilder::build($modelId, $providerName, $request, stream: true);
// Note: stream_options => [include_usage => true] is automatically included.

// 2. The provider package opens the SSE connection and yields raw events.
$events = $this->runner()->streamJson($url, $body, $headers, $providerName);

// 3. The parser converts SSE events to typed StreamPart objects.
$state = new StreamState();
foreach (ChatStreamParser::parse($events, $providerName) as $part) {
    $state->record($part);
}
1

ChatRequestBuilder::build() with stream: true

Produces the same body as the non-streaming path, but adds 'stream' => true and 'stream_options' => ['include_usage' => true] so that token usage is included in the final SSE chunk.
2

SSE Event Stream

The provider package owns the HTTP layer. It reads the response body line by line, parses data: fields, and yields arrays of the shape ['event' => ?string, 'data' => string].
3

ChatStreamParser::parse()

A static generator that iterates the event iterable and yields typed StreamPart objects: ProviderMetadataPart, TextDeltaPart, ReasoningDeltaPart, ToolCallStartPart, ToolCallDeltaPart, and a final FinishPart carrying the resolved FinishReason and accumulated Usage. The [DONE] sentinel is silently ignored.
4

StreamState accumulation

StreamState::record() consumes each StreamPart and builds the final text, tool calls, usage, and provider metadata without the caller needing to inspect individual part types.
Both the standard and streaming paths are synchronous from the caller’s perspective. ChatStreamParser::parse() returns a Generator — iteration is lazy, but there is no Promise, Future, or async framework involved.

Build docs developers (and LLMs) love