DEV Community

Cover image for ACAI — Chapter 9: Multi-Provider AI Integration and Fallback Architecture
Black Shadow Team ©
Black Shadow Team ©

Posted on

ACAI — Chapter 9: Multi-Provider AI Integration and Fallback Architecture

#ai

9.1 Objective

ACAI should not depend permanently on a single AI provider.

A robust architecture should separate:

ACAI Core
   ↓
Provider Interface
   ↓
Provider Adapters
   ├── Cloud Provider A
   ├── Cloud Provider B
   ├── Local Model
   └── Future Providers
Enter fullscreen mode Exit fullscreen mode

The important design principle is:

The core system should know how to request an AI capability, not how every individual provider implements that capability.


9.2 Why a Provider Abstraction Is Necessary

Without an abstraction, the application can become:

Planner
   ↓
OpenAI-specific code
   ↓
OpenAI API
Enter fullscreen mode Exit fullscreen mode

Then changing providers requires modifying many parts of the system.

A better design is:

Planner
   ↓
Model Router
   ↓
Provider Interface
   ↓
Selected Provider
Enter fullscreen mode Exit fullscreen mode

Now the planner does not need to know whether the model is:

Cloud
Local
Open-source
Hosted
Enter fullscreen mode Exit fullscreen mode

9.3 Provider Interface

Create:

app/providers/base.py
Enter fullscreen mode Exit fullscreen mode

Example:

from abc import ABC, abstractmethod


class AIProvider(ABC):

    @abstractmethod
    async def generate(
        self,
        prompt: str,
        **kwargs,
    ) -> str:
        raise NotImplementedError

    @abstractmethod
    async def health_check(
        self,
    ) -> bool:
        raise NotImplementedError
Enter fullscreen mode Exit fullscreen mode

Every provider must implement the same basic contract.


9.4 Provider Adapter

A provider adapter converts ACAI's internal request into the provider's API format.

Architecture:

ACAI Request
     │
     ▼
Provider Interface
     │
     ▼
Provider Adapter
     │
     ▼
External API
Enter fullscreen mode Exit fullscreen mode

This prevents provider-specific implementation details from spreading throughout the application.


9.5 Mock Provider

Before connecting real services, create a mock provider.

Create:

app/providers/mock.py
Enter fullscreen mode Exit fullscreen mode
from app.providers.base import AIProvider


class MockProvider(AIProvider):

    async def generate(
        self,
        prompt: str,
        **kwargs,
    ) -> str:

        return (
            "Mock response generated "
            "for: "
            f"{prompt}"
        )

    async def health_check(
        self,
    ) -> bool:

        return True
Enter fullscreen mode Exit fullscreen mode

This is extremely useful for development because the application can be tested without spending API credits.


9.6 Provider Registry

Create:

app/providers/registry.py
Enter fullscreen mode Exit fullscreen mode
from app.providers.base import AIProvider


class ProviderRegistry:

    def __init__(self):

        self.providers: dict[
            str,
            AIProvider
        ] = {}

    def register(
        self,
        name: str,
        provider: AIProvider,
    ) -> None:

        self.providers[name] = provider

    def get(
        self,
        name: str,
    ) -> AIProvider | None:

        return self.providers.get(name)

    def names(self) -> list[str]:

        return list(
            self.providers.keys()
        )
Enter fullscreen mode Exit fullscreen mode

Example:

registry.register(
    "mock",
    MockProvider(),
)
Enter fullscreen mode Exit fullscreen mode

9.7 Model Router

The model router selects a provider.

Conceptually:

                 REQUEST
                    │
                    ▼
               MODEL ROUTER
                    │
       ┌────────────┼────────────┐
       ▼            ▼            ▼
    Provider A   Provider B   Local Model
       │            │            │
       └────────────┼────────────┘
                    ▼
                 RESULT
Enter fullscreen mode Exit fullscreen mode

The router can consider:

Task type
Model capability
Latency
Cost
Availability
User configuration
Provider health
Enter fullscreen mode Exit fullscreen mode

9.8 Routing Policy

Create:

app/services/model_router.py
Enter fullscreen mode Exit fullscreen mode
class ModelRouter:

    def __init__(
        self,
        registry,
    ):

        self.registry = registry

    async def generate(
        self,
        prompt: str,
        provider_name: str = "mock",
        **kwargs,
    ) -> str:

        provider = (
            self.registry.get(
                provider_name
            )
        )

        if provider is None:

            raise ValueError(
                f"Unknown provider: "
                f"{provider_name}"
            )

        return await provider.generate(
            prompt,
            **kwargs,
        )
Enter fullscreen mode Exit fullscreen mode

This provides one internal interface:

router.generate(...)
Enter fullscreen mode Exit fullscreen mode

regardless of the underlying provider.


9.9 Provider Health

Before selecting a provider, ACAI can check its health.

Conceptually:

Provider A
   │
   ▼
Healthy?
 ┌─┴─┐
YES  NO
 │    │
 ▼    ▼
Use  Fallback
Enter fullscreen mode Exit fullscreen mode

However, repeatedly calling a provider's health endpoint before every request can add latency.

A production system should generally maintain cached health information rather than performing a fresh health check for every generation.


9.10 Provider Status

A provider can have:

healthy
degraded
unavailable
unknown
Enter fullscreen mode Exit fullscreen mode

Example:

class ProviderStatus:

    HEALTHY = "healthy"

    DEGRADED = "degraded"

    UNAVAILABLE = "unavailable"

    UNKNOWN = "unknown"
Enter fullscreen mode Exit fullscreen mode

9.11 Fallback Architecture

Suppose the preferred provider is:

Provider A
Enter fullscreen mode Exit fullscreen mode

and it fails.

The router can attempt:

Provider A
   ↓
Failure
   ↓
Provider B
   ↓
Failure
   ↓
Local Model
Enter fullscreen mode Exit fullscreen mode

Conceptually:

                    REQUEST
                       │
                       ▼
                  Provider A
                       │
                 ┌─────┴─────┐
                 ▼           ▼
              SUCCESS      FAILURE
                 │           │
                 ▼           ▼
               RESULT    Provider B
                              │
                        ┌─────┴─────┐
                        ▼           ▼
                     SUCCESS      FAILURE
                        │           │
                        ▼           ▼
                      RESULT    Local Model
Enter fullscreen mode Exit fullscreen mode

9.12 Fallback Must Be Controlled

Fallback should not mean:

Try providers forever.
Enter fullscreen mode Exit fullscreen mode

It should have:

Maximum attempts
Timeout
Allowed providers
Error classification
Logging
Termination condition
Enter fullscreen mode Exit fullscreen mode

Example:

MAX_PROVIDER_ATTEMPTS = 3
Enter fullscreen mode Exit fullscreen mode

9.13 Provider Error Classification

Not every failure should trigger fallback.

Examples:

Authentication failure
Invalid request
Rate limit
Timeout
Temporary server error
Network error
Enter fullscreen mode Exit fullscreen mode

A policy can classify them as:

Transient
Permanent
Enter fullscreen mode Exit fullscreen mode

For example:

Timeout
→ potentially transient
→ fallback may be appropriate

Invalid request
→ permanent
→ fallback may not solve the underlying problem
Enter fullscreen mode Exit fullscreen mode

9.14 Provider Result Object

Instead of returning only a string, create a structured result.

from dataclasses import dataclass


@dataclass
class ProviderResult:

    text: str

    provider: str

    model: str | None = None

    latency_ms: float | None = None

    success: bool = True

    error: str | None = None
Enter fullscreen mode Exit fullscreen mode

Now ACAI can record how the result was generated.


9.15 Measuring Latency

Use a timer:

import time


start = time.perf_counter()

result = await provider.generate(
    prompt
)

elapsed = (
    time.perf_counter()
    - start
)

latency_ms = elapsed * 1000
Enter fullscreen mode Exit fullscreen mode

This provides measurable provider performance.


9.16 Cost Awareness

Different providers can have different pricing.

The router can eventually maintain metadata:

provider_info = {

    "provider_a": {
        "cost_class": "high",
        "latency_class": "low",
    },

    "provider_b": {
        "cost_class": "low",
        "latency_class": "medium",
    },

    "local": {
        "cost_class": "compute_only",
        "latency_class": "variable",
    },
}
Enter fullscreen mode Exit fullscreen mode

The actual prices should come from current provider pricing documentation rather than being hardcoded as permanent assumptions.


9.17 Task-Aware Routing

Not every task requires the same model.

For example:

Simple classification
→ Smaller model

Complex reasoning
→ More capable model

Local/private processing
→ Local model

High-volume inexpensive task
→ Lower-cost provider
Enter fullscreen mode Exit fullscreen mode

The architecture becomes:

                 TASK
                   │
                   ▼
             TASK CLASSIFIER
                   │
          ┌────────┼────────┐
          ▼        ▼        ▼
        Simple   Complex   Private
          │        │        │
          ▼        ▼        ▼
       Model A   Model B   Local
Enter fullscreen mode Exit fullscreen mode

9.18 Router Policy

A basic policy might be:

ROUTING_POLICY = {

    "simple": [
        "provider_a",
        "provider_b",
    ],

    "complex": [
        "provider_b",
        "provider_a",
    ],

    "private": [
        "local",
    ],
}
Enter fullscreen mode Exit fullscreen mode

The important point is that the routing policy is configuration rather than hardcoded throughout the application.


9.19 Fallback Router

A simple implementation:

class FallbackRouter:

    def __init__(
        self,
        registry,
    ):

        self.registry = registry

    async def generate(
        self,
        prompt: str,
        providers: list[str],
        **kwargs,
    ):

        errors = []

        for name in providers:

            provider = (
                self.registry.get(name)
            )

            if provider is None:

                errors.append(
                    f"{name}: not registered"
                )

                continue

            try:

                result = (
                    await provider.generate(
                        prompt,
                        **kwargs,
                    )
                )

                return result

            except Exception as exc:

                errors.append(
                    f"{name}: {exc}"
                )

        raise RuntimeError(
            "All providers failed: "
            + "; ".join(errors)
        )
Enter fullscreen mode Exit fullscreen mode

This is a prototype and should later be enhanced with explicit timeout and error classification.


9.20 Timeout-Aware Provider Calls

Use:

import asyncio


result = await asyncio.wait_for(
    provider.generate(
        prompt,
        **kwargs,
    ),
    timeout=60,
)
Enter fullscreen mode Exit fullscreen mode

Then a provider that exceeds the limit does not block the complete workflow indefinitely.


9.21 Complete Provider Pipeline

The resulting architecture is:

                         USER REQUEST
                              │
                              ▼
                           PLANNER
                              │
                              ▼
                         TASK TYPE
                              │
                              ▼
                        MODEL ROUTER
                              │
                    ┌─────────┼─────────┐
                    ▼         ▼         ▼
                Provider A Provider B Local
                    │         │         │
                    └─────────┼─────────┘
                              ▼
                           RESULT
                              │
                              ▼
                         VERIFICATION
                              │
                         ┌────┴────┐
                         ▼         ▼
                       PASS      REVISE
                         │         │
                         ▼         ▼
                       FINAL     RETRY
Enter fullscreen mode Exit fullscreen mode

9.22 Local Model Integration

A local provider should follow exactly the same interface.

class LocalProvider(AIProvider):

    async def generate(
        self,
        prompt: str,
        **kwargs,
    ) -> str:

        # Connect to the configured
        # local model server here.

        return "Local model response"

    async def health_check(
        self,
    ) -> bool:

        return True
Enter fullscreen mode Exit fullscreen mode

The important architectural property is:

ACAI Core
   ↓
AIProvider
   ↓
LocalProvider
Enter fullscreen mode Exit fullscreen mode

rather than:

ACAI Core
   ↓
Local-model-specific implementation
Enter fullscreen mode Exit fullscreen mode

everywhere.


9.23 Environment-Based Provider Selection

Configuration:

ACAI_DEFAULT_PROVIDER=mock
ACAI_FALLBACK_PROVIDERS=mock
Enter fullscreen mode Exit fullscreen mode

For development:

mock
Enter fullscreen mode Exit fullscreen mode

For a real deployment:

provider_a
provider_b
local
Enter fullscreen mode Exit fullscreen mode

can be configured according to the actual providers and credentials available.


9.24 API Key Management

Provider credentials must be loaded from environment variables or a secure secret manager.

Example:

PROVIDER_A_API_KEY=...
PROVIDER_B_API_KEY=...
Enter fullscreen mode Exit fullscreen mode

Do not put keys directly into:

Python source
Git repository
Frontend JavaScript
Public documentation
Screenshots
Enter fullscreen mode Exit fullscreen mode

The backend should be the component that communicates with privileged provider APIs.


9.25 Frontend Security

A browser application should never receive a private provider API key simply because the frontend needs to generate an AI response.

Instead:

Browser
   ↓
ACAI Backend
   ↓
Provider API
Enter fullscreen mode Exit fullscreen mode

not:

Browser
   ↓
Private API Key
   ↓
Provider API
Enter fullscreen mode Exit fullscreen mode

9.26 Provider Usage Logging

Each generation should ideally record:

request_id
provider
model
task_type
latency
success
error
Enter fullscreen mode Exit fullscreen mode

Example:

logger.info(
    "AI generation completed",
    extra={
        "request_id": request_id,
        "provider": provider_name,
        "model": model_name,
        "latency_ms": latency_ms,
    },
)
Enter fullscreen mode Exit fullscreen mode

This enables later analysis.


9.27 Provider Metrics

Useful metrics include:

Requests
Success rate
Failure rate
Average latency
P95 latency
Timeout count
Rate-limit count
Fallback count
Enter fullscreen mode Exit fullscreen mode

Example:

Provider A
Success: 98%
Fallback: 2%
Average latency: measured from production
Enter fullscreen mode Exit fullscreen mode

These numbers must come from actual measurements, not assumptions.


9.28 Fallback Metrics

One particularly important metric is:

fallback_rate
Enter fullscreen mode Exit fullscreen mode

If:

1000 requests
50 required fallback
Enter fullscreen mode Exit fullscreen mode

then:

fallback_rate = 5%
Enter fullscreen mode Exit fullscreen mode

A high fallback rate may indicate:

Provider instability
Bad timeout configuration
Rate limiting
Incorrect routing
Credential problems
Network issues
Enter fullscreen mode Exit fullscreen mode

9.29 Testing the Mock Provider

Create:

tests/test_provider.py
Enter fullscreen mode Exit fullscreen mode
import pytest

from app.providers.mock import (
    MockProvider,
)


@pytest.mark.asyncio
async def test_mock_provider():

    provider = MockProvider()

    result = await provider.generate(
        "Hello"
    )

    assert "Hello" in result
Enter fullscreen mode Exit fullscreen mode

Health test:

@pytest.mark.asyncio
async def test_mock_health():

    provider = MockProvider()

    assert (
        await provider.health_check()
        is True
    )
Enter fullscreen mode Exit fullscreen mode

9.30 Testing Fallback

Create a failing provider:

class FailingProvider:

    async def generate(
        self,
        prompt,
        **kwargs,
    ):

        raise RuntimeError(
            "Provider unavailable"
        )
Enter fullscreen mode Exit fullscreen mode

Then configure:

Failing Provider
      ↓
Mock Provider
Enter fullscreen mode Exit fullscreen mode

Expected:

First provider
→ FAIL

Fallback provider
→ SUCCESS
Enter fullscreen mode Exit fullscreen mode

9.31 Fallback Test

Conceptually:

@pytest.mark.asyncio
async def test_fallback():

    registry = ProviderRegistry()

    registry.register(
        "failing",
        FailingProvider(),
    )

    registry.register(
        "mock",
        MockProvider(),
    )

    router = FallbackRouter(
        registry
    )

    result = await router.generate(
        prompt="hello",
        providers=[
            "failing",
            "mock",
        ],
    )

    assert "hello" in result
Enter fullscreen mode Exit fullscreen mode

9.32 Testing All Providers

The system should eventually have tests for:

Provider registration
Unknown provider
Successful generation
Timeout
Transient failure
Permanent failure
Fallback
All providers failing
Health state
Logging
Metrics
Enter fullscreen mode Exit fullscreen mode

9.33 Provider Independence

The most important architectural outcome of Chapter 9 is:

             ACAI CORE
                 │
          AIProvider Interface
                 │
       ┌─────────┼─────────┐
       ▼         ▼         ▼
   Provider A Provider B Local
Enter fullscreen mode Exit fullscreen mode

This means adding another provider should require primarily:

New Adapter
+
Configuration
+
Tests
Enter fullscreen mode Exit fullscreen mode

rather than rewriting the entire application.


9.34 Complete ACAI Architecture

After Chapter 9:

                                  USER
                                    │
                                    ▼
                               API GATEWAY
                                    │
                        ┌───────────┴───────────┐
                        ▼                       ▼
                   RATE LIMITER             REQUEST ID
                        │                       │
                        └───────────┬───────────┘
                                    ▼
                              ORCHESTRATOR
                                    │
                 ┌──────────────────┼──────────────────┐
                 ▼                  ▼                  ▼
              PLANNER            MEMORY            RETRIEVAL
                 │                  │                  │
                 │                  ▼                  │
                 │              DATABASE               │
                 │                                     │
                 └──────────────────┬──────────────────┘
                                    ▼
                              WORKFLOW ENGINE
                                    │
                                    ▼
                               MODEL ROUTER
                                    │
                     ┌──────────────┼──────────────┐
                     ▼              ▼              ▼
                 Provider A      Provider B       Local
                     │              │              │
                     └──────────────┼──────────────┘
                                    ▼
                               GENERATION
                                    │
                                    ▼
                               VERIFICATION
                                    │
                              ┌─────┴─────┐
                              ▼           ▼
                            PASS        REVISE
                              │           │
                              ▼           ▼
                            RESULT      RETRY
                              │
                  ┌───────────┼───────────┐
                  ▼           ▼           ▼
                CACHE        LOGS       METRICS
Enter fullscreen mode Exit fullscreen mode

9.35 What ACAI Can Now Demonstrate

The architecture can now demonstrate a realistic modular AI pipeline:

User
 ↓
Request Validation
 ↓
Planning
 ↓
Workflow Creation
 ↓
Memory / Retrieval
 ↓
Task Routing
 ↓
Provider Selection
 ↓
AI Generation
 ↓
Fallback if appropriate
 ↓
Verification
 ↓
Result
 ↓
Persistent Logging
Enter fullscreen mode Exit fullscreen mode

This is a meaningful engineering architecture.

It is not, by itself, proof of AGI or human-level intelligence.


9.36 Chapter 9 Success Criteria

Chapter 9 is complete when:

[✓] Provider interface exists
[✓] Mock provider works
[✓] Provider registry works
[✓] Model router exists
[✓] Fallback router exists
[✓] Timeout handling exists
[✓] Provider health is defined
[✓] Provider errors are classified
[✓] API keys are externalized
[✓] Provider usage is logged
[✓] Provider metrics are defined
[✓] Fallback is tested
[✓] Local-provider architecture is supported
Enter fullscreen mode Exit fullscreen mode

9.37 Next Chapter

The system now has:

Planning
Retrieval
Memory
Routing
Verification
Workflow
Persistence
Infrastructure
Multi-provider execution
Fallback
Enter fullscreen mode Exit fullscreen mode

The next major problem is evaluation.

An AI system cannot be considered reliable merely because:

API returns 200
Enter fullscreen mode Exit fullscreen mode

or:

Model generated text
Enter fullscreen mode Exit fullscreen mode

We need to measure whether the system actually performs its intended tasks correctly.

Therefore the next chapter is:

Chapter 10 — Evaluation, Benchmarking, Quality Measurement, and Research Validation

It will cover:

Evaluation datasets
Test cases
Task success metrics
Accuracy
Factuality
Latency
Reliability
Regression testing
Human evaluation
Automated evaluation
Benchmark design
A/B testing
Failure analysis
Reproducibility
Research reporting
Enter fullscreen mode Exit fullscreen mode

The architecture will move from:

"It works."
Enter fullscreen mode Exit fullscreen mode

to:

"Here is what it does,
how well it does it,
where it fails,
and how we measured it."
Enter fullscreen mode Exit fullscreen mode

End of Chapter 9

Top comments (0)