Providers and Models

A reference tour of the six clients: how each is created, what it can do, how they differ and what to watch out for. At the end we look at how to ask for the list of models a provider currently offers, and why that is more useful than it sounds.

Six Clients, One Interface

Five clients talk to a specific provider, the sixth to anything that speaks the same dialect as OpenAI. They are all created the same way, with a key in the constructor:

$client = new AIAccess\Provider\OpenAI\Client($apiKey);
$client = new AIAccess\Provider\Claude\Client($apiKey);
$client = new AIAccess\Provider\Gemini\Client($apiKey);
$client = new AIAccess\Provider\DeepSeek\Client($apiKey);
$client = new AIAccess\Provider\Grok\Client($apiKey);

What differs is the set of interfaces each client implements. That is how you tell what to expect from it, and PHP checks it for you before the script even runs:

Client Chat\Service Embedding\Service Batch\Service Image\Service
OpenAI
Gemini
Claude
Grok
DeepSeek
Generic

When you write code that should work with any provider, type the parameter against the interface rather than the concrete class:

public function __construct(
	private AIAccess\Chat\Service $client,
) {
}

Your application then knows nothing at all about the choice of provider, and switching is a change in configuration.

How They Differ

The set of interfaces is only half the story. These are the traits you meet in practice.

OpenAI has the widest reach and is the only one that can upload files (uploadFile(), uploadContent()), which batch processing uses. It reports a refusal separately, so you reach it through getRefusal(). Through setOptions() you can also set the organization.

Claude requires a cap on the answer's length with every request, so the library fills in maxOutputTokens as 4096 unless you set it otherwise. Anthropic offers no embeddings at all, so search will need a different provider. It can count tokens in advance with countTokens().

Gemini has no separate endpoint for image generation: image models are called through ordinary chat; you merely say the answer should be a picture. It also has countTokens(). Batch processing and image generation, however, require a project with active billing; on the free tier a batch ends in an error and image models have a daily quota of zero. A generated image arrives as JPEG, not PNG.

Grok accepts images as input but not documents. It can generate images too, though without reference pictures. It reports a refusal in a message field of its own rather than as a finish reason, which the library translates into FinishReason::ContentFiltered.

DeepSeek is the narrowest of the five: chat only, no vision, embeddings or batches. It is worth knowing that thinking is on by default, so you pay extra tokens until you turn it off.

The Generic Client for Everything Else

Plenty of services today speak the same dialect as OpenAI. OpenAICompatible\Client is there for those, and besides the key you also give it an address:

$client = new AIAccess\Provider\OpenAICompatible\Client($apiKey, 'https://api.mistral.ai/v1/');
$response = $client->createChat('mistral-large-latest')->sendMessage('Hello!');

Ollama running locally wants no key, so send it an empty string:

$client = new AIAccess\Provider\OpenAICompatible\Client('', 'http://localhost:11434/v1/');
echo $client->createChat('llama3.2')->sendMessage('Hello!')->getText();

OpenRouter brokers models from dozens of vendors and likes headers identifying the application:

$client = new AIAccess\Provider\OpenAICompatible\Client($apiKey, 'https://openrouter.ai/api/v1/');
$client->setOptions(extraHeaders: ['HTTP-Referer' => 'https://example.com', 'X-Title' => 'My application']);

Azure OpenAI sends the key in a header of its own and without a prefix:

$client = new AIAccess\Provider\OpenAICompatible\Client($apiKey, 'https://myinstance.openai.azure.com/openai/v1/');
$client->setOptions(authHeader: 'api-key', authPrefix: '');

With the generic client one limitation deserves saying out loud: the library sends what it knows how to send, but what the endpoint does with it is not guaranteed. Tool calls, structured output and images as input all go out, yet whether the model behind that address handles them is decided by that service, not by us. When the endpoint knows an extra parameter, you push it through with the custom argument in options.

Streaming from the generic client sends no stream_options, because that is an OpenAI invention and an unknown dialect may refuse the whole request over it. If your endpoint knows it and you want token usage from the stream, add it through custom.

Which Models a Provider Currently Offers

Because a model name is an ordinary string, nothing checks it for you: a typo or a retired model shows up only at runtime, and as you will see in a moment, sometimes not even then. So ask the provider for the current offering:

foreach ($client->listModels() as $model) {
	echo $model->id, "\n";
}

All six clients have the method and handle pagination internally, so you get the whole list at once. Besides id, every model carries all the metadata in raw exactly as the provider sent it; those are not unified, because each one sends something different.

It is most useful as a deployment check. A retired model need not announce itself with an error: xAI, for instance, quietly redirects old names to a newer model, so the application keeps running and merely talks to something other than you thought. Checking against listModels() is the only way you find out.

Claude and Gemini can additionally count how many tokens a conversation will cost before you send it:

$chat = $client->createChat('claude-sonnet-5');
$chat->addMessage($longText, AIAccess\Chat\Role::User);

echo 'This question will cost ', $chat->countTokens(), " input tokens.\n";

It comes in handy when you assemble a long context and need to know whether it fits the model's limit, or what it will cost, before you pay for it.

Where to Go Next