The PHP AI SDK is built in three distinct layers. The innermost layer,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.
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
Core Contracts — aisdk/core
Defines the portable types that flow across the entire stack without any provider dependency:
- Request types —
TextModelRequest,ImageRequest - Response types —
TextModelResponse,ImageResponse - Message & content —
Message,Content,ContentSource,InputEncoding - Tooling —
Tool,ToolChoice,ToolCall - Stream parts —
TextDeltaPart,ReasoningDeltaPart,ToolCallStartPart,ToolCallDeltaPart,FinishPart,ProviderMetadataPart - Support —
FinishReason,Usage,Reasoning
aisdk/core imports an HTTP client or knows about JSON field names.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.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.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.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.Streaming Data Flow
Whenstream: true is passed, the wire adapter emits a PHP Generator of typed StreamPart objects. The caller accumulates them with StreamState.
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.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].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.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.