{"slug": "i-got-tired-of-switching-ai-sdks-every-time-i-wanted-to-try-a-new-model", "title": "I got tired of switching AI SDKs every time I wanted to try a new model", "summary": "A developer built a thin abstraction layer to unify AI provider APIs after spending a weekend refactoring code to switch from OpenAI's GPT-4 to Anthropic's Claude 3. The solution uses an abstract base class with provider-specific implementations, allowing seamless model swapping without rewriting code.", "body_md": "A few months ago I was building a personal project that needed to generate structured data from natural language. I started with OpenAI's GPT-4 because, well, everyone does. The code worked, the responses were great, and I thought I was done. Then Anthropic released Claude 3, and the benchmarks looked promising. I wanted to try it—just swap one model for another to compare quality and cost.\n\nThat turned into an entire weekend of refactoring.\n\nDifferent SDKs. Different authentication. Different response objects. Even the way you handle streaming (or don't) changed completely. By the end I had a messy pile of `if provider == \"openai\": ... elif provider == \"anthropic\": ...`\n\nblocks that made me feel like I'd written JavaScript in 2014.\n\nI knew I couldn't be the only one dealing with this. Every week there's a new model or a new API. The idea of being locked into one provider felt both brittle and inefficient. So I set out to build a thin abstraction that would let me swap AI providers without rewriting my entire codebase.\n\nMy first instinct was to just use environment variables and conditionally import the right SDK. Something like this:\n\n``` python\nimport os\n\nprovider = os.getenv(\"AI_PROVIDER\", \"openai\")\n\nif provider == \"openai\":\n    from openai import OpenAI\n    client = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\nelif provider == \"anthropic\":\n    from anthropic import Anthropic\n    client = Anthropic(api_key=os.getenv(\"ANTHROPIC_API_KEY\"))\n```\n\nThis worked... until I needed to call the API. The method signatures were completely different:\n\n```\n# OpenAI\nresponse = client.chat.completions.create(\n    model=\"gpt-4\",\n    messages=[{\"role\": \"user\", \"content\": \"Hello\"}]\n)\n\n# Anthropic\nresponse = client.messages.create(\n    model=\"claude-3-opus-20240229\",\n    max_tokens=1024,\n    messages=[{\"role\": \"user\", \"content\": \"Hello\"}]\n)\n```\n\nDifferent parameter names (`messages`\n\nvs `messages`\n\n, okay same—but `max_tokens`\n\nvs `max_tokens`\n\n? Actually Anthropic uses `max_tokens`\n\n, OpenAI uses `max_tokens`\n\ntoo. Wait, that's not the problem. The real pain is the response format: OpenAI returns `response.choices[0].message.content`\n\n, Anthropic returns `response.content[0].text`\n\n. Streaming is even more divergent.\n\nI quickly realized that conditionally importing the client wasn't enough. I needed a unified interface.\n\nI created a simple abstract base class that defines a standard way to send a prompt and get a response. Then I wrote one concrete implementation per provider. The rest of my code only ever talks to the abstract class.\n\nHere's a stripped-down version (I removed error handling and streaming for clarity, but the same pattern applies):\n\n``` python\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\n\n@dataclass\nclass AIResponse:\n    content: str\n    model: str\n    usage: dict | None = None\n\nclass AIProvider(ABC):\n    @abstractmethod\n    def complete(self, prompt: str, **kwargs) -> AIResponse:\n        pass\n```\n\nThen for OpenAI:\n\n``` python\nimport openai\n\nclass OpenAIProvider(AIProvider):\n    def __init__(self, api_key: str, model: str = \"gpt-4\"):\n        self.client = openai.OpenAI(api_key=api_key)\n        self.model = model\n\n    def complete(self, prompt: str, **kwargs) -> AIResponse:\n        response = self.client.chat.completions.create(\n            model=self.model,\n            messages=[{\"role\": \"user\", \"content\": prompt}],\n            **kwargs\n        )\n        return AIResponse(\n            content=response.choices[0].message.content,\n            model=response.model,\n            usage=dict(response.usage) if response.usage else None\n        )\n```\n\nAnd for Anthropic:\n\n``` python\nimport anthropic\n\nclass AnthropicProvider(AIProvider):\n    def __init__(self, api_key: str, model: str = \"claude-3-haiku-20240307\"):\n        self.client = anthropic.Anthropic(api_key=api_key)\n        self.model = model\n\n    def complete(self, prompt: str, **kwargs) -> AIResponse:\n        # Anthropic requires max_tokens; we default to a reasonable value if not provided\n        max_tokens = kwargs.pop(\"max_tokens\", 1024)\n        response = self.client.messages.create(\n            model=self.model,\n            max_tokens=max_tokens,\n            messages=[{\"role\": \"user\", \"content\": prompt}],\n            **kwargs\n        )\n        return AIResponse(\n            content=response.content[0].text,\n            model=response.model,\n            usage=None  # Anthropic doesn't return usage in the same way\n        )\n```\n\nNow I can use a factory function to pick the right provider at startup:\n\n``` python\ndef create_provider(provider_name: str, api_key: str, model: str | None = None) -> AIProvider:\n    if provider_name == \"openai\":\n        return OpenAIProvider(api_key, model or \"gpt-4\")\n    elif provider_name == \"anthropic\":\n        return AnthropicProvider(api_key, model or \"claude-3-haiku-20240307\")\n    # Add more as needed\n    else:\n        raise ValueError(f\"Unknown provider: {provider_name}\")\n\n# Usage\nprovider = create_provider(\"anthropic\", os.getenv(\"ANTHROPIC_API_KEY\"))\nresponse = provider.complete(\"Tell me a joke about Python.\")\nprint(response.content)\n```\n\nThat's it. My application code never touches `openai`\n\nor `anthropic`\n\ndirectly. If I want to try a new provider tomorrow, I just write a new class and add one line to `create_provider`\n\n.\n\nLet me be honest about the limitations. Not all models support the same features. OpenAI has function calling, Anthropic has tool use (similar but not identical). Streaming APIs differ wildly. Token limits vary. Some providers support system messages, others don't. If you try to abstract everything into a single interface, you either end up with a leaky abstraction or you have to support only the lowest common denominator.\n\nMy approach works fine for simple text generation tasks (chat, summarization, classification). But if you rely on advanced features like structured outputs with JSON mode or vision, you'll need to handle those separately—maybe by adding optional methods to the base class that providers can implement or raise `NotImplementedError`\n\n.\n\nAlso, there's a cost side. Different providers charge differently, and you might want to route requests to the cheapest model for a given task. That's a whole other layer of complexity.\n\nI'd look for existing libraries that solve this problem. There are some good ones out there, like `litellm`\n\nor even `langchain`\n\n(though langchain can be heavy). The product I found while researching—something called Interwest AI ([https://ai.interwestinfo.com/)—actually](https://ai.interwestinfo.com/)%E2%80%94actually) provides a unified API for multiple models, which would have saved me the weekend of writing provider classes. But building it myself taught me how each SDK really works, which was valuable.\n\nIf I were starting fresh today, I'd probably use a lightweight wrapper library that normalizes the API, but still keep my own abstract class around in case I need to add a custom provider that the library doesn't support.\n\n`if/elif`\n\nchains.This pattern has saved me hours every time I explore a new model. My side project now has three providers configured, and I can switch between them with a single environment variable change.\n\nWhat's your setup look like? Are you using a wrapper library, rolling your own, or just committing to one provider? I'd love to hear what works (or doesn't) for you.", "url": "https://wpnews.pro/news/i-got-tired-of-switching-ai-sdks-every-time-i-wanted-to-try-a-new-model", "canonical_source": "https://dev.to/__c1b9e06dc90a7e0a676b/i-got-tired-of-switching-ai-sdks-every-time-i-wanted-to-try-a-new-model-51o", "published_at": "2026-07-04 10:00:40+00:00", "updated_at": "2026-07-04 10:18:56.276684+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-tools"], "entities": ["OpenAI", "Anthropic", "GPT-4", "Claude 3"], "alternates": {"html": "https://wpnews.pro/news/i-got-tired-of-switching-ai-sdks-every-time-i-wanted-to-try-a-new-model", "markdown": "https://wpnews.pro/news/i-got-tired-of-switching-ai-sdks-every-time-i-wanted-to-try-a-new-model.md", "text": "https://wpnews.pro/news/i-got-tired-of-switching-ai-sdks-every-time-i-wanted-to-try-a-new-model.txt", "jsonld": "https://wpnews.pro/news/i-got-tired-of-switching-ai-sdks-every-time-i-wanted-to-try-a-new-model.jsonld"}}