DEV Community

Khaled Hammrouni
Khaled Hammrouni

Posted on

05 - 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.'
    ]
];
Enter fullscreen mode Exit fullscreen mode

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."
    );
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  • Offline development. Point at the mock provider and develop/test the whole agent flow with zero API spend and zero network. No .env required.
  • Cost control. Route high-volume, low-stakes calls to a cheap/fast model; reserve the expensive one for the hard paths.
  • Resilience. Wrap in a fallback: try the primary, catch Throwable, retry on a secondary provider. Same agent object shape.
  • A/B and evals. Run the same task against several providers and compare outputs programmatically - this loop is an eval harness.

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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.


Part of the NanoAgent examples series. Landing + demos.

Top comments (0)