Chapter 3 Β· CORE

Provider Adaptation Layer

πŸ“„ 03_provider_adaptation_layer.md 🏷 Core

Chapter 3: Provider Adaptation Layer

Welcome to the third chapter of the cc-switch tutorial!

In the previous chapter, Intelligent Routing & Failover, we gave our gateway the ability to switch providers instantly if one fails.

However, we left a major problem unsolved. Even if we route a request from Claude Code to OpenAI, they might not understand each other. It's like calling a person who speaks French and handing the phone to someone who only speaks Japanese.

In this chapter, we will build the Provider Adaptation Layerβ€”the universal translator for your AI tools.

The Problem: Every API is Different

While most AI providers (Anthropic, OpenAI, Google) offer similar features, their "access points" are different.

  1. Different URLs: Anthropic uses /v1/messages. OpenAI uses /v1/chat/completions.
  2. Different Headers: Anthropic needs an x-api-key. OpenAI needs Authorization: Bearer ....
  3. Different JSON: One expects max_tokens, the other max_completion_tokens.

If cc-switch simply forwards the raw request from one to the other, the destination API will reject it with an error.

The Solution: The "Travel Adapter"

We use the Adapter Pattern. Think of this like a universal travel plug adapter.

Key Concepts

To make this work in Rust, we use a Trait. A Trait is like a contract. It says: "If you want to be a Provider in this system, you must know how to do these specific things."

We defined this contract in adapter.rs. It breaks down the translation process into small steps.

1. The Adapter Contract (ProviderAdapter)

Every adapter (whether for OpenAI, Azure, or a custom gateway) implements this interface.

// src-tauri/src/proxy/providers/adapter.rs

pub trait ProviderAdapter: Send + Sync {
    // 1. Identify yourself (for logging)
    fn name(&self) -> &'static str;

    // 2. Figure out the base address
    fn extract_base_url(&self, provider: &Provider) 
        -> Result<String, ProxyError>;
        
    // ... more functions below
}

2. Authentication Normalization

Every provider handles passwords (API Keys) differently. The adapter's job is to inject the key into the request in the exact format the provider demands.

// src-tauri/src/proxy/providers/adapter.rs

    // 3. Find the API key in the settings
    fn extract_auth(&self, provider: &Provider) -> Option<AuthInfo>;

    // 4. Inject the key into the HTTP headers
    fn add_auth_headers(&self, req: RequestBuilder, auth: &AuthInfo) 
        -> RequestBuilder;

3. Transformation (The Advanced Part)

Sometimes, just changing the URL isn't enough. We might need to rewrite the actual message body (JSON) from Anthropic format to OpenAI format.

// src-tauri/src/proxy/providers/adapter.rs

    // 5. Does this provider need JSON translation?
    fn needs_transform(&self, _provider: &Provider) -> bool {
        false // Default is "No", just pass it through
    }

    // 6. The logic to rewrite the JSON body
    fn transform_request(&self, body: Value, _prov: &Provider) 
        -> Result<Value, ProxyError> {
        Ok(body) // Default is to return the body unchanged
    }

Internal Implementation: The Request Flow

Let's trace what happens when your CLI tool sends a request that needs adaptation.

Sequence Diagram

sequenceDiagram participant CLI as Claude Code participant GW as Gateway participant ADAPT as Adapter participant EXT as External API CLI->>GW: POST /v1/messages (Anthropic Format) GW->>ADAPT: intercept(request) note right of ADAPT: 1. Normalize URL ADAPT->>ADAPT: rewrite URL to target provider note right of ADAPT: 2. Inject Headers ADAPT->>ADAPT: add "Authorization: Bearer sk-..." note right of ADAPT: 3. Transform Body ADAPT->>ADAPT: convert JSON fields ADAPT->>EXT: POST /v1/chat/completions (OpenAI Format) EXT-->>GW: Response GW-->>CLI: Response

How the Code Uses It

When the proxy server receives a request, it doesn't hard-code logic for OpenAI. Instead, it asks the ProviderAdapter to do the work.

Here is a simplified look at how the handler uses the adapter:

// Inside the request handler (simplified)

// 1. Get the correct adapter for the chosen provider
let adapter = provider_factory.get_adapter(&provider.type);

// 2. Build the new URL using the adapter's logic
let url = adapter.build_url(&provider.base_url, "/v1/messages");

// 3. Create the HTTP request
let mut request = client.post(url);

// 4. Ask the adapter to add authentication headers
if let Some(auth) = adapter.extract_auth(&provider) {
    request = adapter.add_auth_headers(request, &auth);
}

Beginner Note: This pattern makes our code very clean. The handler doesn't care which provider is being used. It just follows the steps: Build URL -> Add Auth -> Send.


Managing Complexity: Universal Presets

While the Rust backend handles the logic of adaptation, the configuration needs to come from the user.

In cc-switch, we support "Universal Providers" (like NewAPI or custom gateways) that are compatible with multiple tools. We define these presets in the frontend code so the UI knows how to display them.

Defining Presets (universalProviderPresets.ts)

This TypeScript file defines templates for providers that are known to work well.

// src/config/universalProviderPresets.ts

export const universalProviderPresets: UniversalProviderPreset[] = [
  {
    name: "NewAPI",
    providerType: "newapi", // Tells Rust which adapter to use
    defaultApps: {
      claude: true, // Works with Claude
      codex: true,  // Works with Codex
      gemini: true, // Works with Gemini
    },
    // ... icons and descriptions
  },
];

When a user selects "NewAPI" in the dashboard:

  1. The Frontend reads this preset.
  2. It sends the configuration to the Rust backend.
  3. The Rust backend looks at providerType: "newapi".
  4. It selects the NewApiAdapter (which behaves like an OpenAI standard adapter) to handle traffic.

Why This Matters

Without the Provider Adaptation Layer:

With the Adapter:

Summary

In this chapter, we learned:

  1. APIs speak different languages (URLs, Headers, JSON).
  2. We use the Adapter Pattern to standardize these differences.
  3. We defined a Rust Trait (ProviderAdapter) that acts as a contract for all providers.
  4. We configured "Universal Presets" in the frontend to make setup easy.

Now that our system can route traffic (Chapter 2) and translate languages (Chapter 3), we need a place to remember the user's settings, API keys, and usage history.

In the next chapter, we will dive into the database.

Next Chapter: SQLite Persistence & Schema


Generated by Code IQ