thinwrap/llm

thinwrap/llm for PHP — one typed Chat facade over 18 LLM providers (15 OpenAI-compatible + native Anthropic, Bedrock, Gemini), plus embeddings on 11. Stateless, SDK-free, BYO PSR-18 client — the in-process complement to an LLM gateway.

PHP constraints

Runtime dependencies
php-http/discovery, psr/http-client, psr/http-factory
BYO HTTP client
consumer-supplied PSR-18 client via php-http/discovery
Provenance
cosign-signed GitHub Release artifacts
Minimum runtime
PHP 8.2+
  • PSR-17/PSR-18 discovery is auto-detected; consumers can also pass clients explicitly

30-second install

composer require thinwrap/llm

2-minute end-to-end example

One Chat facade, every provider. Switching vendor is theLlmProviderId case, the config class, and the model — thecomplete($input) call and the ChatResult stay identical.

use Thinwrap\Llm\Chat;
use Thinwrap\Llm\DTO\Chat\ChatInput;
use Thinwrap\Llm\DTO\Chat\ChatMessage;
use Thinwrap\Llm\Enum\ChatRole;
use Thinwrap\Llm\Enum\LlmProviderId;
use Thinwrap\Llm\Exception\ConnectorError;
use Thinwrap\Llm\Providers\Shared\OpenAiCompatConfig;

$chat = new Chat(LlmProviderId::OpenAI, new OpenAiCompatConfig(apiKey: getenv('OPENAI_API_KEY')));

try {
    $res = $chat->complete(new ChatInput(
        model: 'gpt-4o-mini',
        messages: [new ChatMessage(ChatRole::User, 'Say hi in one word.')],
    ));
    echo $res->message->content;
    echo $res->usage->inputTokens . '/' . $res->usage->outputTokens;
} catch (ConnectorError $e) {
    error_log($e->providerCode->value . ': ' . ($e->providerMessage ?? ''));
}

The 15 first-class OpenAI-compatible providers all takeOpenAiCompatConfig; anthropic,bedrock, and gemini are native adapters with their own config classes. Same ->complete($input) shape throughout.

use Thinwrap\Llm\Providers\Anthropic\AnthropicConfig;

$anthropic = new Chat(LlmProviderId::Anthropic, new AnthropicConfig(apiKey: getenv('ANTHROPIC_API_KEY')));
// same ->complete($input) shape, same ChatResult

Supported providers

18 providers ship with thinwrap/llm@1.0.0.
ProviderOperationsAuthREADME
openai OpenAIchat, embeddingsbearerdocs
azure-openai Azure OpenAIchat, embeddingsheaderdocs
anthropic Anthropicchatheaderdocs
gemini Google Geminichatheaderdocs
bedrock AWS Bedrockchataws-sig-v4docs
mistral Mistralchat, embeddingsbearerdocs
cloudflare Cloudflare Workers AIchat, embeddingsbearerdocs
together Together AIchat, embeddingsbearerdocs
fireworks Fireworks AIchat, embeddingsbearerdocs
deepinfra DeepInfrachat, embeddingsbearerdocs
openrouter OpenRouterchat, embeddingsbearerdocs
groq Groqchatbearerdocs
deepseek DeepSeekchatbearerdocs
xai xAI (Grok)chatbearerdocs
perplexity Perplexitychatbearerdocs
vllm vLLMchat, embeddingsbearerdocs
ollama Ollamachat, embeddingsbearerdocs
lmstudio LM Studiochat, embeddingsbearerdocs

Migration paths

The in-process complement to a gateway

Thinwrap LLM is a thin, in-process, SDK-free facade — it does not proxy your traffic. It normalizes the request/response shape across vendors so your code stays vendor-neutral; routing, budgets, and observability stay wherever you already run them. Provider-specific request fields ride through _passthrough; the decoded vendor body is always on the result's raw.

Embeddings

The OpenAI-float subset (11 of the 18 providers — see the table above) also exposes an Embeddings facade with the same provider-switching model.

Bring your own PSR-18 client

A PSR-18 client + PSR-17 factories are auto-discovered viaphp-http/discovery, or pass one explicitly. The wrapper holds no state — retry, timeout, and the client lifecycle stay with the consumer.

Cross-language siblings

The TypeScript (/packages/llm-ts) and Go (/packages/llm-go) variants ship the same facade shape and provider set — cross-language parity is a design goal, not a hard release gate.

Read more