How to Build AI Applications That Switch Models Automatically — Opportunihub
Course Remote

How to Build AI Applications That Switch Models Automatically

Chidiebere Njoku · Remote

At a glance

Type
Course
Organisation
Chidiebere Njoku
Location
Remote
Work mode
Remote
Deadline
Rolling / not stated
Posted
29 Jul 2026

About this course

<p>Large Language Models (LLMs) have fundamentally changed how we build modern software.</p> <p>But relying on a single AI model for every user request creates serious production risks. API outages happen. Proprietary models can be expensive for simple tasks. And cheaper open-source models might struggle with complex logical reasoning.</p> <p>When my team and I built an enterprise-grade AI engine for our customer support platform, we relied on a single top-tier model for everything.</p> <p>Within a month, we faced two massive issues: a widespread API outage completely froze our app, and our monthly API bill rose because we used expensive reasoning models to answer simple FAQs.</p> <p>To fix this, I built a resilient, multi-model orchestrator. In this guide, you'll learn how to build an intelligent, multi-tiered AI application using Python that routes prompts dynamically and handles model fallbacks automatically.</p> <ul> <li><p><a href="#heading-what-well-cover">What We'll Cover</a></p> </li> <li><p><a href="#heading-prerequisites-and-environment-setup">Prerequisites and Environment Setup</a></p> <ul> <li><p><a href="#heading-package-installation">Package Installation</a></p> </li> <li><p><a href="#heading-local-directory-structure">Local Directory Structure</a></p> </li> <li><p><a href="#heading-environment-configuration">Environment Configuration</a></p> </li> </ul> </li> <li><p><a href="#heading-the-problem-with-single-model-architectures">The Problem with Single-Model Architectures</a></p> </li> <li><p><a href="#heading-understanding-the-dynamic-model-routing-lifecycle">Understanding the Dynamic Model Routing Lifecycle</a></p> </li> <li><p><a href="#heading-step-1-implementing-tier-1-prompt-complexity-amp-intent-analysis">Step 1: Implementing Tier 1 – Prompt Complexity &amp; Intent Analysis</a></p> <ul> <li><a href="#heading-breaking-down-the-code-logic-for-tier-1">Breaking Down the Code Logic for Tier 1</a></li> </ul> </li> <li><p><a href="#heading-step-2-implementing-tier2-dynamic-model-routing-logic">Step 2: Implementing Tier2– Dynamic Model Routing Logic</a></p> <ul> <li><a href="#heading-breaking-down-the-code-logic-for-tier-2">Breaking Down the Code Logic for Tier 2</a></li> </ul> </li> <li><p><a href="#heading-step-3-implementing-tier3-automatic-fallbacks">Step 3: Implementing Tier3 – Automatic Fallbacks</a></p> <ul> <li><p><a href="#heading-breaking-down-the-code-logic-for-tier-3">Breaking Down the Code Logic for Tier 3</a></p> </li> <li><p><a href="#heading-combining-the-architecture-into-a-unified-execution-pipeline">Combining the Architecture into a Unified Execution Pipeline</a></p> </li> <li><p><a href="#heading-breaking-down-the-code-logic">Breaking Down the Code Logic</a></p> </li> </ul> </li> <li><p><a href="#heading-lessons-learnt-from-dynamic-model-switching-in-production">Lessons Learnt from Dynamic Model Switching in Production</a></p> </li> <li><p><a href="#heading-conclusion">Conclusion</a></p> <ul> <li><a href="#heading-thank-you-for-reading">Thank You for Reading!</a></li> </ul> </li> </ul> <h2 id="heading-prerequisites-and-environment-setup">Prerequisites and Environment Setup</h2> <p>To follow along with this tutorial, you should have the following setup:</p> <ul> <li><p>Basic proficiency with Python and asynchronous programming.</p> </li> <li><p>Python 3.9 or higher installed on your system.</p> </li> <li><p>A code editor such as Visual Studio Code.</p> </li> <li><p>API keys for at least two model providers (for example, OpenAI and Anthropic), or local models running via Ollama.</p> </li> </ul> <h3 id="heading-package-installation">Package Installation</h3> <p>Open your terminal and install the required dependencies:</p> <pre><code class="language-shell">pip install openai anthropic python-dotenv pydantic </code></pre> <h3 id="heading-local-directory-structure">Local Directory Structure</h3> <p>Organize your project directory like this to keep your code clean:</p> <pre><code class="language-plaintext">ai-model-router/ │ ├── .env ├── README.md └── app.py </code></pre> <h3 id="heading-environment-configuration">Environment Configuration</h3> <p>Create a <code>.env</code> file in the root of your project directory and add your credentials:</p> <pre><code class="language-plaintext">Ini, TOML OPENAI_API_KEY=your_openai_api_key_here ANTHROPIC_API_KEY=your_anthropic_api_key_here ENVIRONMENT=development </code></pre> <h2 id="heading-the-problem-with-single-model-architectures">The Problem with Single-Model Architectures</h2> <img src="https://cdn.hashnode.com/uploads/covers/6a36420b52a5a7620def7e19/cbed7def-7a57-45c9-926a-1d6dce2aabb7.png" alt="A flow diagram illustrating a single-model AI architecture processed by one language model, creating a single point of failure and limiting cost optimization." style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <p>If you route every query to a flagship model like GPT-4o or Claude 3.5 Sonnet, you'd be overspending on simple tasks. Conversely, if you route everything to a smaller, faster model like GPT-4o-mini or Claude 3.5 Haiku to save money, your system will fail when users submit complex code-generation or analytical tasks.</p> <p>On top of cost concerns, single-model systems suffer from single points of failure. When an API provider goes down or rate-limits your account, your entire application crashes.</p> <p>To solve this, you need an orchestration layer that evaluates prompt complexity before invoking an LLM, routes the request to the most cost-effective model, and falls back to a secondary provider if the primary provider fails.</p> <h2 id="heading-understanding-the-dynamic-model-routing-lifecycle">Understanding the Dynamic Model Routing Lifecycle</h2> <img src="https://cdn.hashnode.com/uploads/covers/6a36420b52a5a7620def7e19/e0bc213c-819b-477d-b0fe-e6dd42fac733.png" alt="Flow diagram of a dynamic multi-model AI system with intelligent model selection and automatic failover." style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <p>Here's how a user request journeys through a dynamic multi-model system:</p> <p>First, you have the complexity analysis. The system inspects the incoming prompt using lightweight metrics to assign a task tier (Simple, Medium, or Complex).</p> <p>Second, you have the model routing. The system maps the tier to the appropriate model (for example, lightweight tasks go to Haiku/Mini while heavy reasoning goes to Sonnet/GPT-4o).</p> <p>You also have an automatic fallback: if the primary provider times out or throws an API error, the system automatically redirects the query to an equivalent fallback model.</p> <h2 id="heading-step-1-implementing-tier-1-prompt-complexity-amp-intent-analysis">Step 1: Implementing Tier 1 – Prompt Complexity &amp; Intent Analysis</h2> <p>First, you need a deterministic, fast way to classify prompts without making an expensive API call just to decide which model to use.</p> <p>Before spending money on an LLM API call just to figure out what the user wants, we can look at the text directly in code. Think of this step as a smart gatekeeper. By checking simple things like text length, code snippets, or tricky keywords, we can figure out how hard the task is in milliseconds and for free.</p> <p>Here's how we set up our classification rules inside <code>app.py</code>:</p> <pre><code class="language-python">import re from enum import Enum from pydantic import BaseModel class TaskComplexity(Enum): SIMPLE = "simple" # FAQs, short summaries, basic translation MEDIUM = "medium" # Standard text generation, content rewriting COMPLEX = "complex" # Code writing, math logic, structural analysis class PromptAnalyzer: def __init__(self): # Regex patterns indicative of complex tasks self.complex_keywords = [ r"\brefactor\b", r"\bdebug\b", r"\bwrite code\b", r"\banalyze\b", r"\balgorithm\b", r"\barchitecture\b", ] def analyze_complexity(self, prompt: str) -&gt; TaskComplexity: """ Evaluates input text deterministically to output a TaskComplexity rating. """ normalized = prompt.lower().strip() word_count = len(normalized.split()) # Check for code blocks or complex request patterns contains_code = "```" in prompt has_complex_keyword = any( re.search(pattern, normalized) for pattern in self.complex_keywords ) if contains_code or has_complex_keyword or word_count &gt; 300: return TaskComplexity.COMPLEX elif word_count &gt; 80: return TaskComplexity.MEDIUM else: return TaskComplexity.SIMPLE # Example Usage if __name__ == "__main__": analyzer = PromptAnalyzer() test_prompt = ( "Write a Python script that implements a trie " "data structure with autocomplete." ) complexity = analyzer.analyze_complexity(test_prompt) print(f"Prompt Complexity Tier: {complexity.value}") </code></pre> <h3 id="heading-breaking-down-the-code-logic-for-tier-1">Breaking Down the Code Logic for Tier 1</h3> <ul> <li><p><code>TaskComplexity</code> <strong>Enum:</strong> Defines explicit categories for incoming requests (<code>SIMPLE</code>, <code>MEDIUM</code>, <code>COMPLEX</code>), giving us type safety across our pipeline.</p> </li> <li><p><strong>Keyword Matching:</strong> The <code>PromptAnalyzer</code> class sets up regex patterns looking for action words like <code>refactor</code>, <code>debug</code>, or <code>algorithm</code> that signal a heavy reasoning task.</p> </li> <li><p><strong>Deterministic Rules in</strong> <code>analyze_complexity</code><strong>:</strong></p> </li> <li><p>Formatting &amp; Length Check: We clean the string, check for Markdown code blocks (<code>```</code>), and calculate word counts.</p> </li> <li><p>Tier Allocation:</p> <ul> <li><p>If the prompt contains code blocks, trigger words, or exceeds 300 words, it immediately escalates to <code>COMPLEX</code>.</p> </li> <li><p>If it is between 80 and 300 words without code keywords, it maps to <code>MEDIUM</code>.</p> </li> <li><p>Anything shorter defaults to <code>SIMPLE</code>.</p> </li> </ul> </li> </ul> <p>Running this snippet with a complex query checks the text, spots "write code," and outputs:</p> <p>Prompt Complexity Tier: complex</p> <h2 id="heading-step-2-implementing-tier2-dynamic-model-routing-logic">Step 2: Implementing Tier2– Dynamic Model Routing Logic</h2> <p>Now that we can successfully label a prompt as simple, medium, or complex, we need a rulebook to decide which AI model actually handles it.</p> <p>This layer maps each complexity tier to a primary model and a secondary fallback model. For instance, simple queries route to budget models (gpt-4o-mini), while complex requests route to heavyweights (claude-3-5-sonnet).</p> <p>Add this configuration also:</p> <pre><code class="language-python">class ModelConfig(BaseModel): provider: str model_name: str class ModelRouter: def __init__(self): # Map task complexity tiers to primary and fallback models self.routing_table = { TaskComplexity.SIMPLE: { "primary": ModelConfig( provider="openai", model_name="gpt-4o-mini", ), "fallback": ModelConfig( provider="anthropic", model_name="claude-3-5-haiku-20241022", ), }, TaskComplexity.MEDIUM: { "primary": ModelConfig( provider="openai", model_name="gpt-4o-mini", ), "fallback": ModelConfig( provider="anthropic", model_name="claude-3-5-haiku-20241022", ), }, TaskComplexity.COMPLEX: { "primary": ModelConfig( provider="anthropic", model_name="claude-3-5-sonnet-20241022", ), "fallback": ModelConfig( provider="openai", model_name="gpt-4o", ), }, } def get_models_for_tier( self, complexity: TaskComplexity ) -&gt; tuple[ModelConfig, ModelConfig]: """ Returns the primary and fallback models for a given task complexity tier. """ config = self.routing_table[complexity] return config["primary"], config["fallback"] </code></pre> <h3 id="heading-breaking-down-the-code-logic-for-tier-2">Breaking Down the Code Logic for Tier 2</h3> <ul> <li><p><code>ModelConfig</code> <strong>Schema:</strong> Uses Pydantic to ensure every model definition includes both a <code>provider</code> (for example, <code>"openai"</code>) and a specific <code>model_name</code> string.</p> </li> <li><p><code>self.routing_table</code> <strong>Mapping:</strong> This dictionary acts as our single source of truth for model assignments:</p> <ul> <li><p><code>SIMPLE</code> <strong>&amp;</strong> <code>MEDIUM</code> <strong>Tiers:</strong> Primary target is <code>gpt-4o-mini</code> for high-throughput, low-cost output. If OpenAI fails, it falls back to Anthropic's <code>claude-3-5-haiku-20241022</code>.</p> </li> <li><p><code>COMPLEX</code> <strong>Tier:</strong> Primary target flips to <code>claude-3-5-sonnet-20241022</code> for top-tier code generation and reasoning, with <code>gpt-4o</code> as the backup.</p> </li> </ul> </li> <li><p><code>get_models_for_tier</code><strong>:</strong> A helper function that takes the analyzed tier and safely returns a tuple of <code>(PrimaryModel, FallbackModel)</code>.</p> </li> </ul> <h2 id="heading-step-3-implementing-tier3-automatic-fallbacks">Step 3: Implementing Tier3 – Automatic Fallbacks</h2> <p>Even the best AI providers experience downtime, rate limits, or unexpected timeouts. A production-ready app can't just throw an error screen at the user when this happens. We need an execution engine that attempts to call the primary model provider and automatically catches errors. If anything goes wrong, it instantly pivots to the secondary fallback model without breaking the workflow .</p> <p>Add the execution engine code to the script:</p> <pre><code class="language-python">import os import time from anthropic import Anthropic, APIError as AnthropicAPIError from dotenv import load_dotenv from openai import OpenAI, APIError as OpenAIAPIError load_dotenv() class ResilientModelEngine: def __init__(self): self.openai_client = OpenAI( api_key=os.getenv("OPENAI_API_KEY", "dummy") ) self.anthropic_client = Anthropic( api_key=os.getenv("ANTHROPIC_API_KEY", "dummy") ) def _call_openai(self, model: str, prompt: str) -&gt; str: response = self.openai_client.chat.completions.create( model=model, messages=[ { "role": "user", "content": prompt, } ], timeout=10.0, ) return response.choices[0].message.content def _call_anthropic(self, model: str, prompt: str) -&gt; str: response = self.anthropic_client.messages.create( model=model, max_tokens=1024, messages=[ { "role": "user", "content": prompt, } ], timeout=10.0, ) return response.content[0].text def execute_provider_call( self, config: ModelConfig, prompt: str, ) -&gt; str: """ Dispatches prompt execution to the correct provider SDK. """ if config.provider == "openai": return self._call_openai(config.model_name, prompt) elif config.provider == "anthropic": return self._call_anthropic(config.model_name, prompt) else: raise ValueError( f"Unsupported provider: {config.provider}" ) def execute_with_fallback( self, primary: ModelConfig, fallback: ModelConfig, prompt: str, ) -&gt; tuple[str, str]: """ Attempts execution on the primary model and switches to the fallback model if the primary provider fails. Returns: tuple[str, str]: (Response text, Model used) """ try: print( f"[Attempt] Calling Primary Provider: " f"{primary.provider} ({primary.model_name})" ) result = self.execute_provider_call(primary, prompt) return result, ( f"{primary.provider}:{primary.model_name}" ) except ( OpenAIAPIError, AnthropicAPIError, Exception, ) as e: print(f"[WARNING] Primary call failed due to: {e}") print( f"[Fallback] Switching to Secondary Provider: " f"{fallback.provider} ({fallback.model_name})" ) try: result = self.execute_provider_call( fallback, prompt, ) return result, ( f"{fallback.provider}:" f"{fallback.model_name} (Fallback)" ) except Exception as fallback_error: raise RuntimeError( "Both primary and fallback systems failed. " f"Error: {fallback_error}" ) </code></pre> <h3 id="heading-breaking-down-the-code-logic-for-tier-3">Breaking Down the Code Logic for Tier 3</h3> <p>Provider Clients (<code>_call_openai</code> &amp; <code>_call_anthropic</code>): Helper methods wrap provider SDK calls, establishing a unified strict 10-second timeout. If an API hangs, it aborts fast so the fallback can kick in without making the user wait.</p> <p><code>execute_provider_call</code> Dispatcher: Acts as an abstraction bridge, matching the requested provider string to its respective API method.</p> <p><code>execute_with_fallback</code> Resiliency Logic: Executes the primary provider first inside a try block. Catches API errors, rate limits, or network timeouts via provider-specific exceptions (OpenAIAPIError, AnthropicAPIError). Logically redirects execution to the fallback provider inside the except block. Only raises an unrecoverable <code>RuntimeError</code> if both primary and fallback providers fail. If your primary provider encounters issues, your console tracks the recovery process transparently:</p> <p>[Attempt] Calling Primary Provider: anthropic (claude-3-5-sonnet-20241022)</p> <p>[WARNING] Primary call failed due to: Connection timeout</p> <p>[Fallback] Switching to Secondary Provider: <code>openai</code> (gpt-4o)</p> <h3 id="heading-combining-the-architecture-into-a-unified-execution-pipeline">Combining the Architecture into a Unified Execution Pipeline</h3> <p>Now you can combine all three layers into a unified pipeline.</p> <img src="https://cdn.hashnode.com/uploads/covers/6a36420b52a5a7620def7e19/9070c55d-5c7f-4ab4-b951-9ae6175fed35.png" alt="Unified pipeline for executing AI tasks across multiple models and workflows." style="display:block;margin:0 auto" width="600" height="400" loading="lazy"> <p>Complete your <code>app.py</code> script with this orchestration class:</p> <pre><code class="language-python">class SmartAIEngine: def __init__(self): self.analyzer = PromptAnalyzer() self.router = ModelRouter() self.executor = ResilientModelEngine() def process_request(self, user_prompt: str) -&gt; dict: print("\n==========================================") print("Processing New Request") print("==========================================") # Step 1: Analyze prompt complexity complexity = self.analyzer.analyze_complexity( user_prompt ) print( f"[Step 1] Prompt classified as: " f"{complexity.value.upper()}" ) # Step 2: Determine routing target primary_model, fallback_model = ( self.router.get_models_for_tier( complexity ) ) print( f"[Step 2] Selected Primary: " f"{primary_model.model_name}" ) # Step 3: Execute request with resilient fallbacks response_text, executed_model = ( self.executor.execute_with_fallback( primary=primary_model, fallback=fallback_model, prompt=user_prompt, ) ) return { "status": "success", "complexity_tier": complexity.value, "model_used": executed_model, "response": response_text, } # Execution Pipeline Test if __name__ == "__main__": engine = SmartAIEngine() # Query 1: Simple task simple_query = ( "What is the capital of Japan? " "Answer in one word." ) result_1 = engine.process_request( simple_query ) print(f"Model Used: {result_1['model_used']}") print(f"Response: {result_1['response']}") # Query 2: Complex task complex_query = ( "Write a Python function to debug a " "memory leak in a multithreaded " "application." ) result_2 = engine.process_request( complex_query ) print(f"Model Used: {result_2['model_used']}") print( f"Response Snippet: " f"{result_2['response'][:100]}..." ) </code></pre> <h3 id="heading-breaking-down-the-code-logic">Breaking Down the Code Logic</h3> <ul> <li><p>Unified Orchestration (<code>SmartAIEngine</code>): Initializes all three modular components—<code>PromptAnalyzer</code>, <code>ModelRouter</code>, and <code>ResilientModelEngine</code>—as instance properties.</p> </li> <li><p>The Pipeline Steps:</p> <ul> <li><p>Analyze: Evaluates the prompt string offline to get the complexity tier.</p> </li> <li><p>Route: Resolves primary and secondary model pairs based on that tier.</p> </li> <li><p>Execute: Calls the models resiliently and catches failure scenarios.</p> </li> </ul> </li> <li><p>Normalized Response Payload: Wraps execution details into a consistent output dictionary, keeping track of model usage, complexity categorization, and output text.</p> </li> </ul> <h2 id="heading-lessons-learnt-from-dynamic-model-switching-in-production">Lessons Learnt from Dynamic Model Switching in Production</h2> <p>Building a dynamic AI routing system taught our team critical lessons about enterprise LLM architectures:</p> <p>First, keep classification light. Never use a large LLM call to classify prompts for small tasks. Use regex, keyword matching, and token-length rules. Your classifier should run in under 5 milliseconds.</p> <p>Second, normalize system outputs. Different model providers structure outputs differently. Make sure your application wraps responses in a consistent schema before returning data to the user interface.</p> <p>Third, set a tight timeout. Provider APIs often hang instead of throwing immediate errors. Set tight request timeouts (5 to 10 seconds) on your primary model calls so your fallback triggers quickly without frustrating the end user.</p> <p>And finally, track usage metrics. Log every routing decision, model fallback, and cost delta. This data will reveal whether your complexity thresholds are properly tuned over time.</p> <h2 id="heading-conclusion">Conclusion</h2> <p>As AI applications scale, relying on a single, monolithic LLM becomes unsustainable. Intelligent model routing allows you to balance performance, latency, and cost without sacrificing response quality.</p> <p>By decoupling your application from specific model providers and introducing automated routing layers, input evaluation, provider abstraction, and resilient fallbacks, you can build production AI systems that are cost-effective, fast, and resilient.</p> <p>As you deploy your own applications, treat LLM providers as dynamic utilities. Use lightweight models for everyday processing, reserve flagship models for complex tasks, and handle provider transitions cleanly in code.</p> <h3 id="heading-thank-you-for-reading">Thank You for Reading!</h3> <p>I hope this article has given you a practical understanding of how multi-model orchestrators and dynamic routing work in real-world applications and how you can begin implementing them in your own projects.</p> <p>If you'd like to discuss AI engineering, Agentic AI, LLMs, RAG, MLOps, enterprise AI architecture, or AI governance, feel free to follow, like, share, and connect with me:</p> <ul> <li><p><a href="https://www.linkedin.com/in/chidiebere-njoku-921579142/">LinkedIn</a></p> </li> <li><p><a href="https://github.com/ChidiebereNjoku?tab=repositories">Explore my Github repositories</a></p> </li> </ul>

How to apply

  1. 1 Read the full details above and confirm you meet the eligibility criteria.
  2. 2 Prepare your documents — an updated CV, and any cover letter, proposal or certificates required.
  3. 3 Click Apply on official site to complete your application on Chidiebere Njoku’s official page.
  4. 4 Submit as early as possible — many close once filled.
Apply on official site

Sourced from freecodecamp. Always verify details on the official website. Opportunihub never charges you to apply.

Frequently asked questions

How do I apply for How to Build AI Applications That Switch Models Automatically?

Review the full details and eligibility on this page, prepare your documents, then use the “Apply on official site” button to complete your application on Chidiebere Njoku’s official page.

Is this opportunity remote or location-based?

This opportunity is remote-friendly and open to applicants who can work from anywhere.

Is How to Build AI Applications That Switch Models Automatically free to apply for?

Opportunihub lists this Course for free. Legitimate Courses do not ask for payment to apply — never pay a fee to submit an application.