unlikelysource / simplified-openrouter-php-sdk
Unofficial PHP SDK for the OpenRouter.ai API, modeled after the official Python SDK.
Package info
github.com/dbierer/simplified_openrouter_php_sdk
pkg:composer/unlikelysource/simplified-openrouter-php-sdk
Requires
- php: ^8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.5
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
An unofficial PHP SDK for the OpenRouter.ai API, modeled after the official Python SDK. This is a hand-written, idiomatic PHP client, not a generated 1:1 port — covering the core of the OpenRouter API:
- Chat Completions — including streaming
- Models — list, get, count
- Endpoints — list provider endpoints for a model
- Generations — request/usage metadata lookup
- Credits — account balance
- API Keys — full CRUD (management key required)
- Embeddings
- Images (
/images) and Speech / TTS with voice cloning (/audio/speech), each with model-fallback helpers
The Python SDK is auto-generated from OpenRouter's OpenAPI spec and covers ~90 endpoint groups
(TTS/STT, video generation, OAuth, workspaces, BYOK, datasets, guardrails, analytics, and more).
This simplified PHP SDK, by design, covers only the subset most developers need. The architecture
(a Transportclass plus one resource class per endpoint group) makes it straightforward to add
more resources later if you need them. Contributions welcome!
This SDK leverages the Guzzle HTTP client, and produces PSR-7 compliant requests and responses. IMPORTANT: this is an Alpha release. If you need something more stable and production-ready, consider using eatzy/openrouter-php-sdk instead.
Requirements
- PHP 8.1+
- Composer
Installation
composer require unlikelysource/simplified-openrouter-php-sdk
Quick start
use OpenRouter\Client; use OpenRouter\DTO\ChatMessage; // Reads OPENROUTER_API_KEY from the environment if not passed explicitly. $client = new Client(apiKey: 'sk-or-...'); $response = $client->chat->create([ 'model' => 'openai/gpt-4o-mini', 'messages' => [ ChatMessage::system('You are a helpful assistant.'), ChatMessage::user('Say hello in three languages.'), ], ]); echo $response->getContent();
Streaming
$stream = $client->chat->createStreamed([ 'model' => 'openai/gpt-4o-mini', 'messages' => [ChatMessage::user('Count to 5.')], ]); foreach ($stream as $chunk) { echo $chunk->getContentDelta(); }
createStreamed() returns a ChatCompletionStream, an IteratorAggregate that lazily parses the
API's text/event-stream response and yields one ChatCompletionChunk per server-sent event,
stopping automatically at the [DONE] sentinel.
Request parameters
chat->create() / chat->createStreamed() accept the request body as a plain associative array,
matching the OpenRouter Chat Completions API
directly (model, messages, temperature, max_tokens, tools, tool_choice,
provider, reasoning, response_format, etc.) — nothing is hidden behind a rigid DTO, so any
parameter the API supports can be passed through as-is. messages entries may be plain arrays or
OpenRouter\DTO\ChatMessage instances (ChatMessage::system(), ::user(), ::assistant(), ::tool()).
Resources
// Models $models = $client->models->list(['limit' => 20, 'category' => 'programming']); foreach ($models as $model) { echo "{$model->id}: {$model->contextLength} tokens\n"; } $model = $client->models->get('openai', 'gpt-4o'); $count = $client->models->count(); // Endpoints (which providers serve a given model) $endpoints = $client->endpoints->forModel('openai', 'gpt-4o'); // raw decoded array // Generations $generation = $client->generations->get('gen-abc123'); echo $generation->totalCost; // Credits (requires a management API key) $credits = $client->credits->get(); echo $credits->getRemaining(); // API keys (requires a management API key) $key = $client->apiKeys->create(['name' => 'my-app-key', 'limit' => 10.0]); echo $key->key; // plaintext key — only ever available on the create() response $client->apiKeys->update($key->hash, ['disabled' => true]); $client->apiKeys->delete($key->hash); foreach ($client->apiKeys->list() as $k) { /* ... */ } // Embeddings $result = $client->embeddings->create([ 'model' => 'openai/text-embedding-3-small', 'input' => 'Hello, world!', ]); $vector = $result->data[0]->vector;
Images and speech
use OpenRouter\DTO\InputReference; // Image generation; tries each model in order. input_references is optional (likeness/style images). $img = $client->images->generateWithFallback( ['qwen/qwen-image-3-pro', 'qwen/qwen-image-3'], ['prompt' => 'A lighthouse at dawn', 'size' => '1024x1024', 'input_references' => [InputReference::imageFromFile('ref.png')]], ); file_put_contents('out.png', $img->first()->bytes()); echo $img->model; // the model that actually answered // Text-to-speech with a cloned voice (reference clip + its exact transcript). $speech = $client->speech->createWithFallback( ['fish-audio/s2.1-pro-free:free', 'fish-audio/s2.1-pro'], ['input' => 'Hello there.', 'input_references' => InputReference::voice('me.wav', 'exact words spoken in me.wav')], ); $speech->save('hello.mp3');
images->generate() and speech->create() make a single call with the model you pass. speech->createWithFallback() retries 408/429/5xx and bad payloads per model before moving to the next. Set a generous timeoutSeconds on the Client (e.g. 300) for image and TTS calls.
See examples/ for complete runnable scripts.
Configuration
use OpenRouter\Client; use OpenRouter\Http\RetryConfig; $client = new Client( apiKey: 'sk-or-...', baseUrl: 'https://openrouter.ai/api/v1', // e.g. https://eu.openrouter.ai/api/v1 for EU in-region routing httpReferer: 'https://myapp.example', // sent as HTTP-Referer, used for OpenRouter app rankings title: 'My App', // sent as X-Title timeoutSeconds: 60.0, retryConfig: new RetryConfig(maxAttempts: 4, initialIntervalMs: 500, maxIntervalMs: 60000), );
You can also inject your own Guzzle client (e.g. with custom middleware or a mock handler for
testing) via the httpClient constructor argument.
By default the wait between retries is a blocking usleep(). To use the SDK from an event loop or
a PHP Fiber scheduler, pass a sleeper closure to RetryConfig; it receives the delay in
milliseconds and is used for the transport's backoff and for speech->createWithFallback():
$retryConfig = new RetryConfig( maxAttempts: 3, sleeper: function (int $ms): void { $wake = microtime(true) + $ms / 1000; while (microtime(true) < $wake) { \Fiber::suspend(); // let the scheduler run other requests meanwhile } }, );
Error handling
Requests that receive an HTTP error status throw a subclass of OpenRouter\Exceptions\ApiException,
mapped by status code (BadRequestException, UnauthorizedException, PaymentRequiredException,
ForbiddenException, NotFoundException, TooManyRequestsException, InternalServerException, etc.):
use OpenRouter\Exceptions\ApiException; try { $client->chat->create(['model' => 'openai/gpt-4o', 'messages' => [...]]); } catch (ApiException $e) { echo $e->getStatusCode(); // e.g. 429 echo $e->getMessage(); // the API's error message echo $e->getMetadata(); // provider-specific error metadata, if any }
Connection-level failures (DNS, timeouts, refused connections) that persist after retries are
exhausted throw OpenRouter\Exceptions\TransportException instead.
5xx responses are retried automatically with exponential backoff (defaults: up to 4 attempts, starting at 500ms, capped at 60s, 1.5x multiplier) before an exception is raised.
Development
composer install
composer test
License
MIT. Not affiliated with or endorsed by OpenRouter.