AI Access: One PHP Interface for OpenAI, Claude, Gemini, DeepSeek and Grok
AI Access is a PHP library that unifies working with language models. Instead of five different APIs and five different response formats, you write one piece of code that talks to ChatGPT from OpenAI, Claude from Anthropic, Gemini from Google, DeepSeek and Grok from xAI. Switching between them is a one-line change.
It covers the whole workflow: conversation, streaming responses, tool calling (function calling), structured output following a JSON schema, images and documents as input, image generation, embeddings for search, and batch processing at half the price.
It has no dependencies. Just plain PHP 8.3 and curl, no vendor SDK and no version conflicts with the rest of your project.
What a Language Model Is Good For in an Application
If you have never called an AI API, the principle is simpler than it looks from outside. You send text and the model sends text back. Everything else is built on top of that single move.
In practice this grows into a surprisingly wide range of tasks, and it pays to know them before you start coding, because they tell you which part of this documentation you actually need:
- Writing and rewriting text. Summarizing an article, drafting a reply to an e-mail, translating, proofreading. The most common and simplest case; an ordinary conversation is all you need.
- Classification and decisions. Is this registration spam? Which category does this question belong to? The model answers in one word and you act on it.
- Pulling data out of unstructured text. From an invoice, a CV or an e-mail you need fields you can store in a database. That is what structured output is for: you hand the model a JSON schema and it sticks to it.
- Answering over your own data. The model knows nothing about your documentation, but attach the relevant excerpts to the question and it answers precisely. Finding those excerpts is a job for embeddings, the meaning-based search that people call RAG.
- Working with images and documents. Describe what is in a photo, read the figures off a receipt, summarize an attached PDF. See images and documents as input.
- Actions, not just text. The model can ask for your function to be called, receive the result and carry on. That is how you get an assistant that really looks into your database instead of inventing the answer. See tool calling.
- Bulk processing. When you do not need the answer now, batch processing gets you the same models for half the price.
What AI Access is not: it is not an agent framework, it does not try to write your prompts for you, and it does not keep conversations in a database. It is the layer that talks to the providers' APIs, and it ends exactly where your application's own decisions begin.
Installation
composer require ai-access/ai-access
Requires PHP 8.3 or later and the curl, json and fileinfo extensions, which are available almost everywhere.
The First Message
You need a key from the provider you want to use. They are issued in their consoles: OpenAI, Anthropic, Google, DeepSeek and xAI.
$client = new AIAccess\Provider\OpenAI\Client($apiKey);
$response = $client->createChat('gpt-5.6-luna')
->sendMessage('Write a haiku about PHP.');
echo $response->getText();
That is all of it. createChat() opens a conversation over the chosen model, sendMessage() sends a
message and returns the answer.
The model name is an ordinary string, not a constant or an enum. It sounds like a detail, but it means a new model works the day the provider ships it, with no library update to wait for. To check that a model still exists, use the list of models.
Switching Provider Is One Line
This is the library's central promise, so let it be visible right away. The constructor and the model name change, nothing else:
$client = new AIAccess\Provider\Claude\Client($apiKey);
$chat = $client->createChat('claude-sonnet-5');
$client = new AIAccess\Provider\Gemini\Client($apiKey);
$chat = $client->createChat('gemini-3.5-flash-lite');
$client = new AIAccess\Provider\DeepSeek\Client($apiKey);
$chat = $client->createChat('deepseek-v4-flash');
$client = new AIAccess\Provider\Grok\Client($apiKey);
$chat = $client->createChat('grok-4.3');
In a real application you register the client in the DI container, and switching provider becomes a change in configuration rather than in code.
Five APIs That Agree on Nothing
When you write your own wrapper over the providers, the first two look like a couple of hours of work. The trouble starts with the third, because each of them has a different idea of what a conversation with a model looks like. Here is a small sample of the differences:
| what differs | Claude | OpenAI | Gemini | Grok and DeepSeek |
|---|---|---|---|---|
| endpoint | v1/messages |
v1/responses |
:generateContent |
chat/completions |
| authentication | x-api-key header |
Bearer |
x-goog-api-key header |
Bearer |
| request shape | messages[] |
input[] and instructions |
contents[].parts[] |
messages[] |
| what the model role is called | assistant |
assistant |
model |
assistant |
| token usage keys | input_tokens |
the same | promptTokenCount |
prompt_tokens |
| where the finish reason is | stop_reason |
status, then incomplete_details |
finishReason |
finish_reason |
The last row hides a catch worth saying out loud: Gemini never reports in finishReason that the model wants
to call a tool. It stays STOP and the call itself is found among the parts of the answer. Anyone who does not know
this writes code that silently ignores half of what the model said, and never finds out.
There are dozens of such details and none of them is interesting work. AI Access has them solved and tested against real API responses rather than invented JSON.
At the same time it does not pretend the differences are gone. It unifies what the providers genuinely share, and where they differ it tells you through a type or an exception while you are writing the code, not through an error in production.
What the Library Can Do
| Capability | OpenAI | Claude | Gemini | DeepSeek | Grok | Generic client |
|---|---|---|---|---|---|---|
| Conversation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Reasoning effort | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tool calling | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Streaming | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Images as input | ✅ | ✅ | ✅ | ➖ | ✅ | ✅ |
| Documents as input | ✅ | ✅ | ✅ | ➖ | ➖ | ➖ |
| Structured output | ✅ | ✅ | ✅ | ➖ | ✅ | ✅ |
| Image generation | ✅ | ➖ | ✅ | ➖ | ✅ | ➖ |
| Batch processing | ✅ | ✅ | ✅ | ➖ | ➖ | ➖ |
| Embeddings | ✅ | ➖ | ✅ | ➖ | ➖ | ➖ |
| List of models | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Where a minus appears, either the provider has no such API or the library does not wrap it yet.
The last column is the generic client for anything that speaks the chat/completions dialect: Ollama running on
your laptop, Mistral, OpenRouter, Together, vLLM or Azure. The mark means something different there, namely what the library is
able to send; whether it actually works is decided by the endpoint and the model you point it at. The details are with the providers.
Designed, Not Accreted
Provider-specific settings are named arguments, not keys in a shared array. You feel the difference as you type: your IDE offers exactly what that provider supports, instead of an array quietly swallowing a key that goes nowhere. Add strict types everywhere and readonly value objects.
The exception hierarchy is built around the only question that genuinely matters in production, namely whether the call is worth repeating:
try {
$response = $chat->sendMessage('...');
} catch (AIAccess\ApiException $e) {
// the provider said no; $e->getCode() carries the HTTP status
if ($e->getCode() === 429) {
// rate limited, try again shortly
}
} catch (AIAccess\CommunicationException $e) {
// network hiccup or an unreadable response, retrying may help
}
LogicException deliberately sits outside that tree, because a mistake in your own code is not something production
should catch and walk past. The whole hierarchy is covered by the chapter on error handling.
Retrying, by the way, is not something you have to write yourself. The library ships HTTP layer decorators that retry after rate limits and outages, log every request, or cache responses during development so that re-running a script costs you nothing.
Where to Go Next
- Getting started – keys, the first call, and what to do when something does not add up
- Conversation – history, system instruction, reading the answer and the token usage
- Options and reasoning effort – how much thinking you ask the model for
- Streaming – read the answer while the model is still writing it
- Tool calling – the model asks, your code answers, the loop closes itself
- Structured output – a JSON schema instead of pleading in the prompt
- Providers – what each one can do, how they differ, and how to plug in Ollama or OpenRouter
And if you let an AI agent help you write the code, have a look at Nette AI. You will find a Claude Code plugin that teaches the agent Nette, and MCP Inspector, which lets the agent look straight into your running application.