You build an agent on Anthropic. Twelve tools registered, all of them called correctly. Then the monthly API bill lands, someone says “can we run this on Ollama,” you flip one config value, and the agent starts answering questions instead of doing work. Nothing errored. Nothing logged a warning. Seven of your twelve tools were never shown to the model.
That is not a bug. It is ToolProfileResolver doing exactly what it was written to do, and the provider slug you changed is what triggered it. This article covers the eight providers phpClaw ships, the two tool-schema shapes on the wire, what the tool budget does when you switch, and where the config actually lives on each of the 8 adapters.
The eight slugs
phpClaw exposes the same eight provider slugs on every adapter:
anthropic · openai · groq · gemini · mistral · deepseek · ollama · custom
They all satisfy the same contract, ProviderInterface. From the agent loop’s point of view a provider is a thing you hand messages and tool schemas to, and which hands back either text or a tool call. That part genuinely does not change when you switch.
Three things underneath it do change: the shape of the tool schema on the wire, the number of tools the model is allowed to see, and whether you can stream. In that order of increasing consequence.
Two tool-schema shapes, not one
phpClaw does not have a single internal tool format that every provider consumes. It has two, and which one you get is decided by the slug.
Anthropic gets its native shape:
{
"name": "wp_create_post",
"description": "Create a WordPress post.",
"input_schema": { "type": "object", "properties": { } }
}
The five slugs in the OPENAI_COMPATIBLE_PROVIDERS set — openai, groq, gemini, mistral, ollama — get the OpenAI function-calling shape instead:
{
"type": "function",
"function": {
"name": "wp_create_post",
"description": "Create a WordPress post.",
"parameters": { "type": "object", "properties": { } }
}
}
Same tool, same ToolInterface implementation in your PHP, two envelopes — input_schema becomes parameters under a nested function key.
You never write either by hand; ToolRegistry emits the right shape from the slug. It matters the moment you put a proxy in front of the API, tail the outbound request, or debug why a self-hosted OpenAI-compatible endpoint rejects your payload. Gemini and Mistral being served through the OpenAI-compatible path is the detail that surprises people.
Where the key comes from
When you have not explicitly set a provider and key, phpClaw walks a fixed cascade of environment variables and takes the first non-empty one:
ANTHROPIC_API_KEY → OPENAI_API_KEY → GROQ_API_KEY → GEMINI_API_KEY → MISTRAL_API_KEY → DEEPSEEK_API_KEY
First hit wins. Set both ANTHROPIC_API_KEY and OPENAI_API_KEY and you get Anthropic — not because it is better, because it is first. If a colleague adds an Anthropic key to a shared .env for an unrelated service, your OpenAI-configured agent moves to Anthropic on the next deploy. Set the provider explicitly if that would bother you.
Two slugs are not in the cascade at all. ollama needs no key, so there is nothing to detect — you select it explicitly or you never get it. custom is likewise a deliberate selection.
What actually changes: the tool budget
This is the part worth reading twice, because it is the difference between a working agent and a chatty one.
ToolProfileResolver assigns one of three profiles before the tool list is ever sent:
| Profile | Tool cap |
|---|---|
PROFILE_MINIMAL | 5 |
PROFILE_STANDARD | 8 |
PROFILE_FULL | unlimited (0) |
The routing rule is short. Two slugs — ollama and groq — sit in LOCAL_PROVIDERS. Everything else (anthropic, openai, gemini, mistral, deepseek, custom) gets PROFILE_FULL and sees every tool you registered.
The two in LOCAL_PROVIDERS start at PROFILE_MINIMAL — five tools — and rise to PROFILE_STANDARD’s eight only if the configured model name string contains one of seven size patterns:
30b · 32b · 34b · 35b · 65b · 70b · 72b
A substring match on the model name. qwen2.5:32b matches and gets eight. qwen2.5:7b does not and gets five. There is no capability probe, no handshake, no model-card lookup — the model’s own name is the signal, because it is the only signal available before the first request.
The reasoning behind the cap is not defensive. Small locally-served models have smaller context windows and less reliable tool selection than the frontier hosted models, and a long tool menu costs on both counts: every schema is tokens spent before the user’s message is read, and a model shaky at tool choice gets shakier as the menu grows. Capping at five is a deliberate trade — fewer tools available, but the ones available get called correctly more often.
One classification catches people out. Groq is a hosted API. You do not run it on your own hardware, and it is not slow. It is in LOCAL_PROVIDERS anyway — the grouping reads as tracking the class of model served, open-weight models in the 7B–70B range, rather than where inference physically happens. The effect is concrete: point phpClaw at Groq and you are on the five-tool default unless your model name carries one of those seven patterns.
A second, separate filter runs after this one. ToolRouter applies a per-turn keyword-relevance pass on top of whatever the profile allowed, with its own model-specific limits — haiku and flash 6, gpt-3.5 5, mini 8, sonnet 12, gpt-4o 15, opus 20, 10 by default. file_read is always included; pinned tool names go first. So moving from gpt-4o to gpt-4o-mini narrows the per-turn list from 15 to 8 without touching the provider slug. Two sequential filters, two triggers. Both belong to Tools; the provider and model choice made here is what sets them off.
What actually changes: streaming
stream() is implemented on both the Anthropic and the OpenAI providers, so streaming is available on the slugs those two classes serve.
There is a hard limit on Anthropic that you need before you design a UI around it: AnthropicProvider::stream() passes tools: []. Explicitly, an empty array. You cannot stream a response and use tools in the same turn on Anthropic. If your plan was a live-typing chat window whose agent also reads the database, that is two different code paths, not one.
Whether the OpenAI-compatible providers do the same thing during streaming is not confirmed here. Their stream() body was not read for this article. Treat it as an open question — do not assume the same limitation, and do not assume its absence. If your product depends on streaming plus tools on OpenAI, read that method yourself before committing.
What actually changes: the default model
Each slug ships a different default model, so switching the provider silently switches the model too:
| Slug | Default model |
|---|---|
anthropic | claude-haiku-4-5-20251001 |
openai | gpt-4o-mini |
gemini | gemini-1.5-flash |
groq | llama-3.1-8b-instant |
deepseek | deepseek-chat |
mistral | mistral-small-latest |
ollama | qwen2.5:7b |
Note what the Groq and Ollama defaults do to the previous two sections. llama-3.1-8b-instant contains no medium-size pattern. qwen2.5:7b contains none either. Both defaults land on PROFILE_MINIMAL. The out-of-the-box configuration for both local-class providers is the five-tool one, and you have to change the model name — not just the provider — to lift it.
What does not change: there is no failover
ProviderRetryLoop retries the same provider up to maxRetries on a ProviderException, then re-throws. There is no second provider to fall back to. phpClaw does not maintain a provider ranking, does not hold a warm secondary, and will not quietly finish your turn on OpenAI because Anthropic returned 529.
Hallucinated tool-call errors are not retried at all — they re-throw immediately, on the reasoning that asking the same model the same question again is unlikely to produce a different invented tool name.
If you need cross-provider resilience, you build it above phpClaw: catch the exception, swap the provider config, run the turn again. Nothing in the library does it for you, and no configuration flag turns it on.
So which should you use
A real answer, grounded only in the mechanisms above.
Use Anthropic or OpenAI in production, and treat that as the default choice rather than a premium one. Both land on PROFILE_FULL, so the tool list you registered is the tool list the model sees — no cap to reason about, no model-name substring to keep an eye on during upgrades. If your agent has more than eight tools, this is not a preference, it is the only tier that fits. Between the two: Anthropic is first in the auto-detect cascade and its native schema shape needs no translation layer, but it is the one with the confirmed streaming-excludes-tools limit. If a streaming UI with live tool use is central to your product, verify OpenAI’s streaming path yourself and pick on that evidence.
Use Ollama for local development, privacy-constrained deployments, and genuinely cost-sensitive workloads. The cost argument is not a marginal one — a locally-hosted model has no per-token API cost at all, which is a different category of thing from a cheaper per-token rate. For a site running an agent on every page view, that can be the whole decision. The price is stated plainly: five tools by default, eight if you configure a 30b-or-larger model, and tool-calling reliability that is lower than the hosted frontier models — which is the reason the cap exists in the first place. Design the tool set to fit the budget rather than registering twelve and hoping. If a 30b+ model fits your hardware, run one; the jump from five to eight tools is often the difference between a usable agent and a crippled one.
Use Groq when you want open-weight models at speed and you have read the LOCAL_PROVIDERS section above. A hosted API on the minimal profile is an unusual combination, and one that will confuse you at 2am if you have not internalised it.
deepseek, mistral and gemini are all PROFILE_FULL; nothing in phpClaw’s mechanics argues for or against them. custom is for an endpoint phpClaw ships no preset for, and also gets PROFILE_FULL — phpClaw assumes full capability rather than probing for it. If your custom endpoint fronts a 7B model, that assumption is yours to manage.
Where the config lives, per adapter
The split is deliberate and holds across all 8 adapters: CMS adapters store provider config in their own native admin settings storage and never read .env; framework adapters use .env or bundle config with env resolution. A WordPress site owner should not need shell access to change an API key, and a Laravel deployment should not have a secret sitting in the database where a config dump can print it.
| Adapter | Config storage | Notes |
|---|---|---|
| WordPress | wp_options, key phpclaw_settings | — |
| Joomla (5/6, one registration) | #__extensions.params (plugin params) | Legacy per-provider key fields silently fall back if the unified api_key is empty |
| Laravel | .env via config/phpclaw.php | Runs the full six-key auto-detect cascade |
| Symfony | Bundle config / config/phpclaw.yaml, env-resolvable | Same cascade, applied at the ClawConfig layer |
| Drupal (10/11) | Drupal Config API + FormAPI (ConfigFormBase) | No getenv() or $_ENV read anywhere in src/ |
| Magento (2.4+) | core_config_data, path phpclaw/general/* | No system.xml — a custom admin page instead |
| OpenCart (3+4) | {prefix}setting table, code='module_phpclaw' | — |
| PrestaShop (8.2+9) | ps_configuration, PHPCLAW_* keys via Configuration::get() | No env fallback |
Two consequences worth stating. On the six CMS adapters, setting ANTHROPIC_API_KEY in a shell or a .env file does nothing — the adapter is not reading there, and the cascade described earlier applies to the framework adapters. And on Drupal and PrestaShop specifically, the absence of any env fallback has been verified in source, so there is no undocumented back door to configure them through.
Limitations
No cross-provider failover. The headline one. ProviderRetryLoop retries the same provider and then gives up. A sustained outage at your provider is a sustained outage for your agent unless you have written the swap yourself.
Streaming and tools do not combine on Anthropic, confirmed — stream() passes tools: []. For the OpenAI-compatible providers this is not confirmed either way. That is the honest boundary of what is known: one confirmed limitation, one unknown. Do not build on the assumption that OpenAI behaves the same, and do not build on the assumption that it does not.
The tool cliff is silent. This is the limitation most likely to cost you an afternoon. Nothing in phpClaw warns you when the profile cap drops your tool list. There is no console notice, no admin banner, no log line saying “17 tools registered, 5 sent.” A developer who builds against Anthropic and later switches to Ollama for cost reasons loses tools without being told, and the symptom — an agent that describes what it would do instead of doing it — looks like a prompt problem rather than a config one. If you switch to ollama or groq, count your tools first and decide which five or eight matter.
Provider capability is inferred, never probed. The medium-size check is a substring match against seven patterns in the model name. A capable model whose name does not include one of them gets five tools. A weak model whose tag happens to contain 70b gets eight. The heuristic is cheap and usually right; it is still a heuristic, and it is the only one there is.
No per-token cost data. phpClaw does not carry pricing tables and this article will not quote any. The one cost fact that comes from the architecture rather than from a price list: a model you host yourself through Ollama has no per-token API charge at all.
What to read next
- phpClaw Tools: How the Agent Calls Code — where
ToolProfileResolverandToolRouterlive, and what the two filters do to your tool list in sequence. Read it before you finalise a provider choice. - phpClaw Memory: What the Agent Remembers, and What It Costs — the other consumer of the context budget your provider choice sets.
- phpClaw Guard: What Stops a Bad Tool Call — provider-independent, which is the point: the guards run identically whichever backend you picked.
- phpClaw Hooks: The Agent Lifecycle You Can Intervene In — six of the 40 lifecycle events are provider events, which is where you attach your own retry or cost accounting.
- phpClaw Skills: Injecting Domain Knowledge Without Fine-Tuning — the cheapest way to make a small local model behave once the tool budget has left it with fewer tools.