Providers
A provider sends the conversation to a model API and returns the reply. Core ships three native providers and six OpenAI-compatible presets, with no extra package to install.
Choosing a provider
Section titled “Choosing a provider”Set it on the builder:
use PhpClaw\Claw;
$agent = Claw::builder() ->provider('openai') ->apiKey($key) ->model('gpt-4o') ->build();Or leave it out and phpClaw picks from the environment. PHPCLAW_PROVIDER wins if set; otherwise the
first key found in this order decides:
| Order | Environment variable | Provider |
|---|---|---|
| 1 | ANTHROPIC_API_KEY | anthropic |
| 2 | OPENAI_API_KEY | openai |
| 3 | GROQ_API_KEY | groq |
| 4 | GEMINI_API_KEY | gemini |
| 5 | MISTRAL_API_KEY | mistral |
| 6 | DEEPSEEK_API_KEY | deepseek |
| 7 | OLLAMA_HOST | ollama |
With none of them set, the provider is anthropic, and building fails because there is no key.
When ->model() is not called, PHPCLAW_MODEL is used, then the provider’s default.
Built-in providers
Section titled “Built-in providers”| Slug | Class | Default model | Key |
|---|---|---|---|
anthropic | AnthropicProvider | claude-haiku-4-5-20251001 | ANTHROPIC_API_KEY |
gemini | GeminiProvider | gemini-3.5-flash-lite | GEMINI_API_KEY |
openai | OpenAIProvider | gpt-4o-mini | OPENAI_API_KEY |
groq | OpenAIProvider | llama-3.1-8b-instant | GROQ_API_KEY |
mistral | OpenAIProvider | mistral-small-latest | MISTRAL_API_KEY |
deepseek | OpenAIProvider | deepseek-flash | DEEPSEEK_API_KEY |
ollama | OpenAIProvider | qwen2.5:7b | none |
custom | OpenAIProvider | none | OPENAI_API_KEY |
The last six share one class and differ in endpoint, default model, authentication, and whether
native structured output is used (only openai).
Anthropic
Section titled “Anthropic”AnthropicProvider names three models as constants and picks an output limit for each when you do
not set maxTokens():
| Constant | Model | Default max_tokens |
|---|---|---|
MODEL_HAIKU | claude-haiku-4-5-20251001 | 8192 |
MODEL_SONNET | claude-sonnet-5 | 16000 |
MODEL_OPUS | claude-opus-4-8 | 16000 |
| any other model | 4096 |
Prompt caching
Section titled “Prompt caching”$agent = Claw::builder()->provider('anthropic')->promptCache(false)->build();On by default through the builder. It marks the system prompt and the last tool as cacheable
and sends the prompt-caching-2024-07-31 beta header. AnthropicProvider’s own constructor defaults
it to off, so a provider you construct yourself does not cache unless told to.
| Where you run | Prompt caching by default |
|---|---|
Claw::builder() | on |
| WordPress, Joomla, Drupal, Magento, PrestaShop, OpenCart | on: they do not set it, so the builder default applies |
| Laravel | off, PHPCLAW_PROMPT_CACHE |
| Symfony | off, prompt_cache |
Extended thinking
Section titled “Extended thinking”$agent = Claw::builder()->provider('anthropic')->thinkingBudget(2048)->build();
$response = $agent->send('...');echo $response->thinking;A budget of 1024 or more turns it on. It sends thinking: {type: enabled, budget_tokens: N} with
the interleaved-thinking-2025-05-14 beta header, and raises max_tokens to at least the budget plus
1000. Below 1024 it is off.
Laravel exposes it as PHPCLAW_THINKING_BUDGET and Symfony as thinking_budget, both default 0.
No CMS adapter exposes it.
Gemini
Section titled “Gemini”GeminiProvider sends the key as a ?key= query parameter rather than a header.
When you do not set maxTokens(), a model id starting gemini-3.5-flash-lite, gemini-3.5-flash,
gemini-2.5-flash, gemini-2.5-pro or gemini-2 gets an output limit of 8192 tokens. Any other
model gets 2048.
It strips JSON Schema keywords that Gemini rejects before sending tool schemas, including
additionalProperties, $schema, $ref, const and patternProperties, and converts nullable
union types to Gemini’s nullable flag.
Gemini 3 models attach a thoughtSignature to each function call and answer HTTP 400 (“Function call
is missing a thought_signature”) when the next request drops it. The provider keeps the signature
with the tool call and sends it back on the same part. Older models send no signature, so their
requests are unchanged.
Model ids starting gemini-1. are treated as legacy and never receive web search.
OpenAI-compatible presets
Section titled “OpenAI-compatible presets”openai, groq, mistral, deepseek, ollama and custom all use OpenAIProvider against a
Chat Completions endpoint.
Ollama
Section titled “Ollama”No key. The endpoint is http://127.0.0.1:11434/v1/chat/completions, or
OLLAMA_HOST followed by /v1/chat/completions when that variable is set.
export OLLAMA_HOST=http://localhost:11434qwen2.5:7b is the default model, so pull it first or set another:
ollama pull qwen2.5:7bCustom
Section titled “Custom”Any OpenAI-compatible endpoint:
export PHPCLAW_PROVIDER=customexport OPENAI_BASE_URL=https://llm.internal.example.com/v1/chat/completionsexport OPENAI_API_KEY=...export PHPCLAW_MODEL=my-modelWithout OPENAI_BASE_URL, or without a key, building fails. There is no default model, so set one. A
Bearer token is always sent.
An endpoint given with no path, such as https://llm.example.com, gets /v1/chat/completions
appended. One with a path is used as written.
Tool schemas are formatted by provider name. openai, groq, gemini, mistral, ollama,
deepseek and custom receive the OpenAI function shape; anthropic and any unknown name receive
the Anthropic-native shape:
openai, groq, gemini, mistral, ollama, deepseek, custom → {"type": "function", "function": {...}}anthropic, unknown names → {"name": ..., "description": ..., "input_schema": ...}Web search
Section titled “Web search”Anthropic, Gemini and OpenAI support provider-native web search, and it is attached by default. When you configure none, the builder attaches one with no limits.
| Provider | What is sent by default |
|---|---|
anthropic | a web_search tool on every request, with no use limit |
gemini | google_search grounding, but only on a request that carries no function tools |
openai presets | web_search_options, but only when the model id contains search-preview |
Configure it:
use PhpClaw\Providers\Tools\WebSearch;
$agent = Claw::builder() ->provider('anthropic') ->withProviderTool((new WebSearch)->max(3)->allow(['php.net'])->location(country: 'GB')) ->build();| Method | Effect | Honoured by |
|---|---|---|
max(int) | maximum searches per request | Anthropic |
allow(array) | allowed domains | Anthropic |
location(city, region, country) | approximate user location | Anthropic, and the openai presets on a search-preview model |
Gemini’s grounding ignores all three.
The builder has no switch to remove it: configuring no web search means “use the default”. A
provider passed to providerOverride() gets the same default when it supports web search, so only a
provider that does not implement SupportsWebSearchInterface is sent without it.
On Gemini, an agent with any tools registered never gets grounding.
Routing by difficulty
Section titled “Routing by difficulty”ModelRouter asks a model how difficult a message is and returns the provider name you mapped to
that difficulty, ready for provider():
use PhpClaw\Claw;use PhpClaw\Providers\ModelRouter;use PhpClaw\Providers\OpenAIProvider;
$classifier = new OpenAIProvider( apiKey: '', model: 'qwen2.5:7b', endpoint: 'http://127.0.0.1:11434/v1/chat/completions', name: 'ollama', authStyle: 'none',);
$router = new ModelRouter($classifier, ['easy' => 'groq', 'hard' => 'anthropic']);$message = 'What is 2 + 2?';
$agent = Claw::builder()->provider($router->resolve($message))->build();The model is asked for JSON, {"value": "<one of your difficulties>"}. A reply that is not JSON, or
names a difficulty that is not in the map, throws StructuredOutputException; a failed model call
throws ProviderException. Each mapped provider needs its own API key, as with any provider() name.
The choice is made once, before the agent is built; a run never switches provider.
Streaming
Section titled “Streaming”$agent->stream($message, $onToken) behaves differently depending on tools:
| Agent | What $onToken receives |
|---|---|
| no tools registered | tokens as the provider streams them |
| tools registered, or durable runs on | nothing until the whole run finishes, then the final answer in 10-character pieces |
With tools or durable runs, every model call is a normal request so that tool calls can be read, and the finished
answer is replayed through $onToken. The output looks streamed, but the first chunk arrives only
after the full answer exists.
Registering a provider
Section titled “Registering a provider”use PhpClaw\ClawConfig;use PhpClaw\Providers\ProviderRegistry;
ProviderRegistry::register('myllm', fn (ClawConfig $config) => new MyLlmProvider($config->apiKey, $config->model));
$agent = Claw::builder()->provider('myllm')->apiKey($key)->build();register() takes a factory, as above, or a class name. A class is constructed as
new $class($apiKey, new RawHttpClient, new StreamParser, $model), so its constructor must accept
those four positional arguments. Names are case-insensitive.
Your own slug has no key variable of its own, so pass the key with apiKey(). A provider key in the
environment, such as ANTHROPIC_API_KEY, is not picked up for it.
The registry is consulted before the presets, so registering openai or ollama replaces that
preset.
The contract
Section titled “The contract”namespace PhpClaw\Providers\Contracts;
interface ProviderInterface{ public function send(array $messages, array $tools = []): array; public function stream(array $messages, callable $onToken): string; public function name(): string; public function model(): string;}name() decides the tool schema shape, so return one of openai, groq, gemini, mistral,
ollama, deepseek or custom if your API expects OpenAI-style tools, and anything else for the
Anthropic shape.
Testing with a mock
Section titled “Testing with a mock”providerOverride() bypasses provider resolution entirely. It also bypasses the system prompt:
systemPrompt() and, with tools registered, the built-in doctrine are never passed to a provider you
supply. Give your provider its own system prompt.
use PhpClaw\Claw;use PhpClaw\Providers\Contracts\ProviderInterface;
$mock = $this->createMock(ProviderInterface::class);$mock->method('name')->willReturn('anthropic');$mock->method('model')->willReturn('claude-haiku-4-5-20251001');$mock->method('send')->willReturn([ 'type' => 'text', 'text' => 'Hello from mock', 'input_tokens' => 10, 'output_tokens' => 5,]);
$agent = Claw::builder()->providerOverride($mock)->build();
$this->assertSame('Hello from mock', $agent->send('Hello')->text);A mock of ProviderInterface does not implement web search, so no web search is attached.
The catalogue
Section titled “The catalogue”ProviderCatalogue::keys() lists every provider available by slug: the two native providers carrying
a #[Provider] attribute, anthropic and gemini, followed by the six presets, then any provider
added with ProviderCatalogue::register() or declared in a Composer package’s extra.phpclaw.providers.
activateFromSettings() registers nothing; the catalogue is for listing providers, for example in an
admin dropdown.