OpenAI, Claude, Gemini, DeepSeek a Grok se neshodnou skoro na ničem: jiný endpoint, jiná autentizace, jiný tvar požadavku i odpovědi. Knihovna AI Access je sjednocuje do jednoho PHP rozhraní bez jediné závislosti, ve kterém je přechod od jednoho poskytovatele k druhému změnou jednoho řádku.
Jak to začalo
Jsem nadšenec do jazykových modelů a první doláče za API jsem utrácel už v roce 2022. Pro OpenAI existovala komunitní knihovna pro PHP, sice měla víc souborů než celé Nette, ale fungovala 🙂
Pak jsem se začátkem roku 2024 zamiloval do Claude. A Claude žádnou knihovnu pro PHP neměl. Tak jsem si napsal triviální dvě třídy, protože co jiného.
Jenže na jednu věc je vždycky lepší jeden model a na druhou druhý, takže jsem potřeboval přepínat oba poskytovatele, a začal vznikat wrapper. Časem přibyla podpora embeddingů (ty tady třeba doporučují podobné články, jak vidíte pod tímhle textem), pak přišly obrázky, kód se rozrůstal a v jednu chvíli bylo jasné, že vzniká regulérní knihovna.
Přidal jsem ještě podporu pro Gemini, protože jejich cenová politika (zadarmo) mě motivovala protočit tam pár desítek milionů tokenů. Později taky DeepSeek a Grok, u kterých dodnes nevím proč 🙂
V dubnu 2025 jsem knihovně vybral jméno a pustil ji na GitHub ve verzi 0.1.
Od té doby uplynulo šestnáct měsíců. Vydat jedničku znamená slíbit, že se rozhraní jen tak nezmění, a ten slib se těžko dává, když se pod rukama neustále mění, co vlastně modely umí. OpenAI mezitím přesunulo chat na úplně jiný endpoint a s přemýšlejícími modely přišly thought signatures, které se musí vracet nezměněné zpátky. Streamování odpovědí přibylo do knihovny teprve letos v létě, protože pět firem má pět různých představ o tom, jak takový proud dat vlastně skončí.
Teď se to usadilo natolik, že jedničku dávám ven.
Instaluje se přes composer require ai-access/ai-access a chce
PHP 8.3 plus rozšíření curl, json a fileinfo. To je celý seznam
požadavků: žádné Guzzle, žádná sada PSR rozhraní, žádný konflikt
verzí se zbytkem projektu.
Pět API, která se neshodnou na ničem
Když si nad providery píšete vlastní obálku, první dva jsou legrace. U třetího pochopíte, že opět začínáte od nuly:
| co se liší | Claude | OpenAI | Gemini |
|---|---|---|---|
| endpoint | v1/messages |
v1/responses |
:generateContent |
| autentizace | x-api-key |
Bearer |
x-goog-api-key |
| tvar požadavku | messages[] |
input[] a instructions |
contents[].parts[] |
| jak se jmenuje role modelu | assistant |
assistant |
model |
| kde je důvod ukončení | stop_reason |
status, pak incomplete_details |
finishReason |
Takových detailů jsou desítky a ani jeden z nich není zajímavá práce. Je to práce, kterou bych dnes dal AI agentovi.
Přepnutí providera je jedna řádka
Tohle je ústřední motiv knihovny, tak ať je vidět hned. Takhle pracuju s OpenAI:
$client = new AIAccess\Provider\OpenAI\Client($apiKey, chatModel: 'gpt-5.6-luna');
$response = $client->createChat()
->sendMessage('Napiš haiku o PHP.');
echo $response->getText();
A tady je Claude:
$client = new AIAccess\Provider\Claude\Client($apiKey, chatModel: 'claude-sonnet-5');
$chat = $client->createChat();
Klientů je šest: pět pro konkrétní providery a jeden generický pro
všechno, co mluví dialektem OpenAI, tedy Ollama na vašem notebooku,
OpenRouter, Mistral nebo Azure. Co který umí, se pozná podle rozhraní,
která implementuje. AIAccess\Chat\Service mají všichni,
AIAccess\Embedding\Service jen OpenAI a Gemini, a tak dál.
Když píšete kód, který má fungovat s libovolným providerem, typujte parametr na takové rozhraní, nikoliv na konkrétní třídu:
public function __construct(
private AIAccess\Chat\Service $client,
) {
}
Aplikace pak o volbě providera neví vůbec nic a přepnutí je změna v konfiguraci DI kontejneru, ne v kódu:
services:
- AIAccess\Provider\Claude\Client(%anthropicApiKey%, chatModel: 'claude-sonnet-5')
A potřebujete providera, který zvládne konverzaci i obrázky zároveň? Řekněte si o obě rozhraní najednou:
public function __construct(
private AIAccess\Chat\Service&AIAccess\Image\Service $client,
) {
}
(Takovému parametru vyhoví OpenAI, Gemini a Grok.)
Co všechno to umí
Chat je jen začátek. Knihovna pokrývá celý pracovní postup:
- Streaming,
aby uživatel nekoukal deset vteřin na prázdnou stránku. Čte se
foreachem a dá se v půlce zastavit. - Tool calling,
kdy si model řekne o zavolání vaší funkce, dostane výsledek a pokračuje.
Celou smyčku odbaví jediné
sendMessage(). - Structured output, tedy JSON schéma místo prosby v promptu. Tvar odpovědi vynucuje provider, ne dobrá vůle modelu.
- Obrázky a dokumenty na vstupu. Přiložíte fotku účtenky nebo PDF smlouvy a ptáte se na obsah.
- Generování obrázků u OpenAI, Gemini a Grok, ačkoli Gemini žádný endpoint na obrázky nemá a kreslí přes obyčejný chat. Vy o tom nemusíte vědět.
- Embeddingy pro vyhledávání a doporučování podle významu, ne podle shody slov.
- Batch processing za zhruba poloviční cenu, když odpověď nepotřebujete hned.
Abstrakce, která nelže
Sjednotit se dá to, co mají provideři společné. Zbytek ne, a knihovna se netváří, že rozdíly zmizely.
Nastavení, které má jen jeden z nich, je pojmenovaný argument metody
setOptions() na konkrétní třídě, ne klíč ve
sdíleném poli:
$chat->setOptions(maxOutputTokens: 1024, store: false); // OpenAI
Rozdíl oproti poli ucítíte při psaní. IDE vám nabídne přesně to, co daný provider umí, a překlep chytí samo PHP.
Jediná otázka, na které v produkci záleží
Když volání cizího API selže, můžete se ptát na spoustu věcí. V běžící aplikaci rozhoduje jediná: mám to zopakovat, nebo je to marné?
Podle toho jsou postavené výjimky:
CommunicationExceptionznamená, že jsme se nedovolali. Opakování skoro vždy pomůže.ApiExceptionnese vgetCode()HTTP status, takže 429 znamená „počkej a zkus to znovu“, zatímco 401 je špatný klíč a stokrát opakovaný bude pořád špatný.UnexpectedResponseExceptionříká, že odpověď nemá očekávaný tvar. To je na podívání, ne na opakování.LogicExceptionje chyba ve vašem kódu a schválně stojí mimo tenhle strom, protože vlastní chybu nemá produkce odchytávat a jít dál.
Samotné opakování psát nemusíte. Knihovna má dekorátory HTTP vrstvy, které se skládají jako matrjošky:
$client = new AIAccess\Provider\OpenAI\Client(
$apiKey,
new AIAccess\Http\RetryClient(new AIAccess\Http\CurlClient),
);
RetryClient opakuje jen to, co má smysl opakovat, respektuje
hlavičku Retry-After a exponenciálně prodlužuje čekání
s trochou náhody, aby tisíc paralelních procesů neuhodilo na providera
v tutéž chvíli. Nejzajímavější pravidlo se týká streamování: jakmile
dorazil první kus odpovědi, opakování se vypne. Model už píše a vy už za
to platíte, takže druhý pokus by vám odpověď doručil dvakrát a dvakrát
ji naúčtoval.
Vedle toho je ObservableClient, který hlásí každý
požadavek i to, jak dlouho trval, a CachingClient, který při
vývoji nedovolí, abyste za padesáté spuštění téhož skriptu platili
padesátkrát.
A pak je tu dokumentace
Dokumentace má čtrnáct kapitol a je to text, který se dá číst od začátku do konce a po kterém víte, co děláte. Snažil jsem se, aby ho pochopil i laik. Takže třeba vysvětluje, co je vlastně embedding, bez použití lineární algebry.
Jsou tam věci, na které se přijde až v provozu, a to obvykle na
faktuře. Třeba že obrázek v historii konverzace se platí znovu v každém
dalším kole. Říká, proč break ve streamu generování
nezastaví a cancel() ano.
Druhá půlka téhož je adresář examples/ přímo
v distribuci: přes dvacet spustitelných programů v jedenácti tematických
složkách, každý o jednom pojmu. Jsou to skutečné skripty, které berou
providera jako první argument:
php examples/chat/streaming.php openai
php examples/chat/streaming.php gemini
Tentýž soubor tedy mluví s pěti různými firmami a vy vidíte na vlastní oči, že se výstup neliší. Kde nějaký provider funkci nemá, příklad to řekne a slušně skončí, místo aby spadl na chybě z API.
Jděte si hrát
Zkopírujte examples/.env.example na examples/.env,
doplňte klíč providera, kterého chcete zkusit, a pusťte kterýkoli
skript.
Hezké generování 🙂
Přečtěte si určitě také článek Model, který se ptá, si nevymýšlí.
Komentáře
Posp #1
Ahoj,
díky! Jen drobnost – v sekci Přepnutí providera je jedna řádka → A tady je Claude je imho špatně namespace, místo
má být
David Grudl #2
#1 Pospe, opraveno
pHqghUme #3
555
Napište komentář