Sechno
Ai

Build a Provider‑Agnostic AI Client: Swap Models With One API Key (No Code Changes)

A practical guide to building a provider-agnostic AI client that lets you route requests to multiple model providers behind a single API key (or proxy). Includes adapter patterns, capability negotiation, fallback strategies, prompt templating, and tradeoffs.

SSechno Team 5 min read 26 views
Build a Provider‑Agnostic AI Client: Swap Models With One API Key (No Code Changes)

Why a provider-agnostic AI client

Services that offer "one API key for many models" are gaining attention because they simplify secrets management and let teams switch or multiplex backend models without changing app code. The approach is evergreen: whether you use a third-party multiplexer or roll your own adapter layer, designing an API client that hides provider differences improves portability, testing, and resilience.

This article shows a practical implementation pattern you can adapt: a small adapter/strategy layer that normalizes requests and responses, negotiates capabilities (e.g., streaming, multimodal), and provides cost/latency-aware fallbacks.

For background reading, see the original writeup that sparked this trend: One API Key for 14 AI Models — No Code Changes.

Core design goals

  • Normalize a simple request/response contract for your application.
  • Encapsulate provider-specific payloads and authentication.
  • Support capability detection (streaming, image support, max tokens).
  • Provide fallback and cost-based routing policies.
  • Keep prompt templates portable across providers.

Implementation blueprint

  1. Define a small, stable interface your app calls (generate, embeddings, classify).
  2. Implement provider adapters that map generic requests to provider-specific APIs.
  3. Add a factory/router that chooses adapters by policy and handles fallbacks.
  4. Include a capability registry so callers can ask "does provider X support streaming?"
  5. Keep prompt templates and post-processing centralized.

1) The stable interface

Start with a minimal interface your app code uses. Keep payloads generic and lean.

<?php
interface AIClientInterface
{
    /**
     * Generate text or structured output.
     * @param array $req {"prompt": string, "max_tokens": int, "mode": "text|chat"}
     * @return array {"text": string, "meta": array}
     */
    public function generate(array $req): array;
}

2) A base adapter with HTTP helper

Adapters translate the normalized request to provider requests. Keep HTTP concerns in the base class so adapters stay focused on mapping.

<?php
abstract class ProviderAdapter implements AIClientInterface
{
    protected string $baseUrl;
    protected string $apiKey;
 
    public function __construct(string $baseUrl, string $apiKey)
    {
        $this->baseUrl = $baseUrl;
        $this->apiKey = $apiKey;
    }
 
    abstract protected function mapRequest(array $req): array;
    abstract protected function mapResponse(array $resp): array;
 
    public function generate(array $req): array
    {
        $payload = $this->mapRequest($req);
 
        $ch = curl_init($this->baseUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            'Authorization: Bearer ' . $this->apiKey,
        ]);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
 
        $raw = curl_exec($ch);
        $err = curl_error($ch);
        curl_close($ch);
 
        if ($raw === false) {
            throw new \RuntimeException("HTTP error: " . $err);
        }
 
        $resp = json_decode($raw, true) ?? ['raw' => $raw];
        return $this->mapResponse($resp);
    }
}

3) Provider adapters (example)

Concrete adapters implement mapping logic. Keep adaptation small — only transform what's necessary.

<?php
class OpenAIAdapter extends ProviderAdapter
{
    protected function mapRequest(array $req): array
    {
        // Generic <=> provider mapping
        return [
            'model' => $req['model'] ?? 'gpt-4o',
            'prompt' => $req['prompt'],
            'max_tokens' => $req['max_tokens'] ?? 512,
        ];
    }
 
    protected function mapResponse(array $resp): array
    {
        // Normalize to {"text": "...", "meta": {...}}
        $text = $resp['choices'][0]['text'] ?? ($resp['output'] ?? '');
        return ['text' => $text, 'meta' => $resp];
    }
}
 
class AnthropicAdapter extends ProviderAdapter
{
    protected function mapRequest(array $req): array
    {
        // Anthropic-style payload example
        return [
            'model' => $req['model'] ?? 'claude-2',
            'input' => $req['prompt'],
            'max_tokens_to_sample' => $req['max_tokens'] ?? 512,
        ];
    }
 
    protected function mapResponse(array $resp): array
    {
        $text = $resp['completion'] ?? ($resp['output'] ?? '');
        return ['text' => $text, 'meta' => $resp];
    }
}

4) Factory & fallback router

The router picks an adapter based on policy. Policies can be simple (priority list), or more advanced (cost/latency prediction).

<?php
class AIClientFactory
{
    /**
     * $providers: [ ['type'=>'openai','url'=>'https://api.openai.com/v1/…','key'=> 'xxx','score'=>10], ... ]
     */
    public static function create(array $providers): AIClientInterface
    {
        $adapters = [];
        foreach ($providers as $p) {
            switch ($p['type']) {
                case 'openai':
                    $adapters[] = new OpenAIAdapter($p['url'], $p['key']);
                    break;
                case 'anthropic':
                    $adapters[] = new AnthropicAdapter($p['url'], $p['key']);
                    break;
                // add more providers here
            }
        }
 
        return new class($adapters) implements AIClientInterface {
            private array $adapters;
            public function __construct(array $adapters) { $this->adapters = $adapters; }
 
            public function generate(array $req): array
            {
                $lastEx = null;
                foreach ($this->adapters as $adapter) {
                    try {
                        return $adapter->generate($req);
                    } catch (\Exception $e) {
                        // log and try next adapter
                        $lastEx = $e;
                    }
                }
                throw $lastEx ?? new \RuntimeException('No providers configured');
            }
        };
    }
}

5) Prompt templating and portability

Keep prompt templates provider-agnostic. Replace model-specific instructions via normalization layers. An example is a tiny templating helper:

<?php
function renderTemplate(string $tpl, array $vars): string
{
    foreach ($vars as $k => $v) {
        $tpl = str_replace("{{" . $k . "}}", $v, $tpl);
    }
    return $tpl;
}
 
// Usage
$tpl = "Summarize the following text in 3 bullet points:\n\n{{text}}";
$prompt = renderTemplate($tpl, ['text' => "Long article content..."]);

Operational considerations and tradeoffs

  • Latency: Using a router or third-party multiplexer may add hops. Measure latency and add timeouts.
  • Cost & quotas: Providers expose different billing and rate limits. Implement per-provider throttling and cost-aware routing.
  • Capabilities mismatch: Some models support streaming, others don't. Expose capability flags and fall back gracefully.
  • Compliance & auditing: Routing through a third party may affect data residency. Keep audit logs and consider on‑premise adapters for sensitive workloads.
  • Testing: Mock the AIClientInterface in tests. The stable interface keeps tests deterministic.
  • Complexity: The adapter layer adds code and maintenance — weigh this against the cost of changing app code when provider changes.

Deployment patterns

  • Start with a single adapter and a simple priority-based router, then iterate toward cost/capability policies.
  • Expose metrics per provider (latency, success rate, cost) to drive routing decisions.
  • Consider a feature-flagged rollout: route a small % of traffic to a new provider before full switch.

Conclusion

Building a provider-agnostic AI client makes your application resilient to changing model vendors and enables safe experimentation. Keep the interface small and stable, encapsulate provider differences in adapters, negotiate capabilities up front, and implement clear fallback and cost policies. This architecture works with a third-party "one API key" multiplexer or with your own adapter layer.

Want a starter? Use the code snippets above as a minimal skeleton and extend adapters for specific provider features (streaming, images, embeddings). Keep prompt templates portable and test via interface mocks.

Was this helpful?

Share this post

Comments (0)

Want to join the conversation?

Log in or sign up to leave a comment and share your thoughts.

Log in to Comment