Multi-Provider LLM in PHP - Switch Between Groq, OpenAI, and Mock
Hard-coding one LLM provider is a trap. You want to test locally with a mock, run cheap on Groq, and fall back to OpenAI in production - without rewriting the agent. The whole provider choice is one config array.
This example loops over three providers with the identical agent code and shows the switch is genuinely that small.
The providers are just config
Read the default provider/model/key from config once, then lay out three provider configs as a plain array - a free offline mock, your configured default (Groq), and OpenAI as a paid fallback.
use NanoAgent\Agent;
$configFile = __DIR__ . '/../NanoAgent/config.php';
$default = file_exists($configFile) ? require $configFile : [];
$apiKey = $default['api_key'] ?? '';
$provider = $default['provider'] ?? 'groq';
$model = $default['model'] ?? 'llama-3.3-70b-versatile';
$configs = [
'Mock' => [
'llm' => ['provider' => 'mock', 'model' => 'test-model', 'api_key' => 'mock-key'],
'description' => 'Offline mock for local development.'
],
'Groq' => [
'llm' => ['provider' => $provider, 'model' => $model, 'api_key' => $apiKey],
'description' => 'High-performance inference.'
],
'OpenAI' => [
'llm' => ['provider' => 'openai', 'model' => 'gpt-4o', 'api_key' => getenv('OPENAI_API_KEY')],
'description' => 'Commercial provider.'
]
];
The loop - same agent, different brain
Skip any provider you have no key for, then create an Agent per config and send it the same test message. Notice the Agent constructor call is identical every time except for the llm array.
foreach ($configs as $name => $cfg) {
if ($name !== 'Mock' && empty($cfg['llm']['api_key'])) {
echo "$name: skipped (no key)\n";
continue;
}
$agent = new Agent(
llm: $cfg['llm'], // <-- the ONLY thing that changes
systemPrompt: "You are testing the $name provider. Reply briefly identifying yourself."
);
Wrap the call in try/catch so one provider's outage or bad key doesn't stop you from testing the others.
try {
$response = $agent->chat("Verify connection.");
echo "$name: OK -> $response\n";
} catch (Throwable $e) {
echo "$name: ERROR -> {$e->getMessage()}\n";
}
}
The new Agent(llm: ...) line is the seam. Everything downstream - tools, history, streaming, structured output - is provider-agnostic. You never touch the rest of your code to change the model.
What this buys you
mock provider and develop/test the whole agent flow with zero API spend and zero network. No .env required.Throwable, retry on a secondary provider. Same agent object shape.The fallback pattern
$providers = ['groq', 'openai'];
foreach ($providers as $p) {
try {
return (new Agent(llm: ['provider' => $p, 'model' => $model, 'api_key' => $key]))
->chat($message);
} catch (Throwable $e) {
// log and try the next provider
}
}
Bottom line: keep the provider in config, keep the agent code generic, and "switching models" stops being a refactor and becomes a one-line change.
Ready to ship an agent?
composer require hammrouni/nanoagent
MIT, zero dependencies. Explore the full series - each article teaches one pattern.
Get each new article by email - one per publish day + the 12-part cheatsheet when it drops.