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/llm2-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 ChatResultSupported providers
| Provider | Operations | Auth | README |
|---|---|---|---|
openai OpenAI | chat, embeddings | bearer | docs |
azure-openai Azure OpenAI | chat, embeddings | header | docs |
anthropic Anthropic | chat | header | docs |
gemini Google Gemini | chat | header | docs |
bedrock AWS Bedrock | chat | aws-sig-v4 | docs |
mistral Mistral | chat, embeddings | bearer | docs |
cloudflare Cloudflare Workers AI | chat, embeddings | bearer | docs |
together Together AI | chat, embeddings | bearer | docs |
fireworks Fireworks AI | chat, embeddings | bearer | docs |
deepinfra DeepInfra | chat, embeddings | bearer | docs |
openrouter OpenRouter | chat, embeddings | bearer | docs |
groq Groq | chat | bearer | docs |
deepseek DeepSeek | chat | bearer | docs |
xai xAI (Grok) | chat | bearer | docs |
perplexity Perplexity | chat | bearer | docs |
vllm vLLM | chat, embeddings | bearer | docs |
ollama Ollama | chat, embeddings | bearer | docs |
lmstudio LM Studio | chat, embeddings | bearer | docs |
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.