# How to Configure Provider Runtimes Like OpenAI, Claude, and OpenCode with Adapters in Routa

> Configure AI provider runtimes like OpenAI, Claude, and OpenCode using adapters in Routa. Normalize LLM interfaces and handle protocol differences automatically for unified access.

- Repository: [Fengda Huang/routa](https://github.com/phodal/routa)
- Tags: how-to-guide
- Published: 2026-05-26

---

**Routa uses a provider-registry and adapter pattern to normalize different LLM runtimes into a unified interface, handling protocol differences such as immediate versus deferred tool inputs automatically.**

The Routa framework (`phodal/routa`) abstracts the complexities of various AI providers through a centralized registry system and provider-specific adapters. When you configure provider runtimes, you are essentially mapping external LLM protocols—whether from Anthropic Claude, OpenCode, or other ACP-compatible services—into Routa's normalized session-update format. This architecture allows you to switch between providers without rewriting your core application logic.

## Understanding the Provider-Registry Pattern

At the heart of Routa's configuration system lies the **Provider Registry**, a singleton defined in [`src/core/acp/provider-registry.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/provider-registry.ts). This registry maintains factories for each supported provider and exports critical utilities for model resolution and capability queries.

The registry stores several key structures:
- **`PROVIDER_MODEL_TIERS`** – Maps abstract tiers (`fast`, `balanced`, `smart`) to concrete model identifiers per provider
- **`parseCompoundModelId`** – Parses `provider:model` compound strings used throughout the UI and backend
- **`findBestProviderForRole`** – Enables specialist-selection logic based on provider capabilities

When your application initializes, the server runtime registers provider factories using `ProviderRegistry.getInstance().register(providerId, factory)`. This registration binds a provider identifier (e.g., `"claude"`, `"opencode"`) to a constructor function that returns an adapter instance when invoked with configuration options.

## Provider Adapter Architecture

Adapters translate vendor-specific protocols into Routa's normalized `NormalizedSessionUpdate` shape. The critical distinction between adapters lies in how they handle **tool input timing**, controlled via the `immediateToolInput` behavior flag.

### Immediate Input Pattern (Claude Code)

The **Claude Code adapter** ([`src/core/acp/provider-adapter/claude-adapter.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/provider-adapter/claude-adapter.ts)) implements an immediate-input protocol. When Claude emits a `tool_call` event, the input parameters arrive fully formed in the `rawInput` field.

```typescript
import { ClaudeCodeAdapter } from '@/core/acp/provider-adapter/claude-adapter';

const rawNotification = {
  sessionUpdate: "tool_call",
  toolCallId: "c2",
  kind: "write",
  title: "Write File",
  rawInput: { 
    filePath: "/tmp/bar.ts", 
    content: "export const x = 1;" 
  }
};

const adapter = new ClaudeCodeAdapter();
const normalized = adapter.normalize("sess-456", rawNotification);

// normalized.toolCall.inputFinalized === true

```

Because `immediateToolInput` is set to `true` in this adapter, the `inputFinalized` property indicates that no further updates are expected for this tool call.

### Deferred Input Pattern (OpenCode)

Conversely, the **OpenCode adapter** ([`src/core/acp/provider-adapter/opencode-adapter.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/provider-adapter/opencode-adapter.ts)) handles deferred inputs. The initial `tool_call` event contains an empty `rawInput` object, with actual parameters arriving later via `tool_call_update` events.

```typescript
import { OpenCodeAdapter } from '@/core/acp/provider-adapter/opencode-adapter';

// Initial notification - input not yet available
const initial = {
  sessionUpdate: "tool_call",
  toolCallId: "c1",
  kind: "read",
  title: "Read File",
  rawInput: {} // Empty placeholder
};

const adapter = new OpenCodeAdapter();
const firstUpdate = adapter.normalize("sess-123", initial);
// firstUpdate.toolCall.inputFinalized === false

// Later update containing the actual input
const deferred = {
  sessionUpdate: "tool_call_update",
  toolCallId: "c1",
  kind: "read",
  rawInput: { filePath: "/tmp/foo.ts" },
  status: "running"
};

const finalUpdate = adapter.normalize("sess-123", deferred);
// finalUpdate.toolCall.inputFinalized === true

```

The adapter internally tracks pending tool calls through `handleDeferredInput`, updating the session state only when the complete input arrives.

## Factory Pattern for Adapter Resolution

Rather than instantiating adapters directly, use the factory function exported from [`src/core/acp/provider-adapter/index.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/provider-adapter/index.ts):

```typescript
import { getProviderAdapter } from '@/core/acp/provider-adapter';

// Returns ClaudeCodeAdapter, OpenCodeAdapter, or StandardAcpAdapter
const adapter = getProviderAdapter("claude");

```

The `getProviderAdapter` function normalizes provider IDs (case-insensitive, hyphen-aware) and caches instances. Unrecognized providers gracefully fall back to `StandardAcpAdapter`, ensuring backward compatibility.

## UI Integration and Provider Selection

The provider selector UI component ([`src/client/components/chat-panel/components/provider-dropdown.tsx`](https://github.com/phodal/routa/blob/main/src/client/components/chat-panel/components/provider-dropdown.tsx)) consumes the registry to populate available options:

```typescript
import { getRegisteredProviders } from '@/core/acp/provider-registry';

function ProviderDropdown({ onChange }: { onChange: (id: string) => void }) {
  const providers = getRegisteredProviders(); 
  // Returns: ["claude", "opencode", "docker-opencode"]
  
  return (
    <select onChange={e => onChange(e.target.value)}>
      {providers.map(id => (
        <option key={id} value={id}>{id}</option>
      ))}
    </select>
  );
}

```

When a user selects a provider, the UI passes the ID through `resolveModelForSpecialist` before initiating the ACP process, ensuring the correct model tier is selected based on the specialist's requirements.

## Configuring Model Tiers and Specialization

Routa supports intelligent model selection through tier-based resolution. The `resolveModelForSpecialist` function in [`src/core/acp/provider-registry.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/provider-registry.ts) automatically selects appropriate models when specialists request specific capability tiers:

```typescript
import { resolveModelForSpecialist, ModelTier } from '@/core/acp/provider-registry';

const model = resolveModelForSpecialist(
  undefined,                // No specific model requested
  "smart" as ModelTier,     // Prefer high-capability tier
  "claude:sonnet-4.5",      // Parent session model
  "claude"                  // Parent provider
);
// Result: "claude:opus-4.6"

```

This configuration allows parent sessions using Claude Sonnet to spawn specialist sub-agents that automatically upgrade to Claude Opus when the "smart" tier is requested, maintaining provider consistency while optimizing for capability.

## Adding Custom Provider Configurations

To integrate a new runtime (for example, a hypothetical OpenAI adapter), extend the base architecture:

1. **Create the adapter class** implementing `BaseProviderAdapter` with appropriate `immediateToolInput` settings
2. **Register the factory** in the provider registry during server initialization
3. **Update `PROVIDER_MODEL_TIERS`** to include default model mappings for the new provider
4. **Document capabilities** in [`docs/configuration/providers-and-models.md`](https://github.com/phodal/routa/blob/main/docs/configuration/providers-and-models.md)

Each adapter must implement the `normalize(sessionId, rawNotification)` method, returning a `NormalizedSessionUpdate` that the ACP process manager can route uniformly regardless of the underlying LLM runtime.

## Summary

- **Provider Registry** ([`src/core/acp/provider-registry.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/provider-registry.ts)) maintains singleton factories and model-tier mappings for all supported LLM runtimes
- **Adapter Pattern** normalizes protocol differences, with Claude using immediate input and OpenCode using deferred input patterns
- **Factory Resolution** via `getProviderAdapter()` in [`src/core/acp/provider-adapter/index.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/provider-adapter/index.ts) handles case-insensitive provider ID lookup and caching
- **Model Tiers** (`fast`, `balanced`, `smart`) automatically resolve to provider-specific models through `resolveModelForSpecialist`
- **UI Components** consume `getRegisteredProviders()` to dynamically populate selection dropdowns based on currently available adapters

## Frequently Asked Questions

### How does Routa handle different input timing between Claude and OpenCode?

Routa handles timing differences through the `immediateToolInput` behavior flag in each adapter. The **Claude adapter** ([`claude-adapter.ts`](https://github.com/phodal/routa/blob/main/claude-adapter.ts)) sets `inputFinalized: true` immediately upon receiving a `tool_call` event because the `rawInput` contains complete parameters. The **OpenCode adapter** ([`opencode-adapter.ts`](https://github.com/phodal/routa/blob/main/opencode-adapter.ts)) sets `inputFinalized: false` initially and uses `handleDeferredInput` to update the tool call when the subsequent `tool_call_update` arrives with the actual parameters.

### Can I use multiple providers simultaneously in one Routa session?

Yes, the provider registry supports concurrent usage. The `ProviderRegistry` singleton stores factories for all configured providers, and `getProviderAdapter` can return different adapter instances for different session IDs. When spawning specialists, `resolveModelForSpecialist` can switch providers based on capability requirements while maintaining the parent session context, allowing hybrid workflows where Claude handles primary reasoning and OpenCode handles specific tool executions.

### What happens if I request an unrecognized provider ID?

The system falls back gracefully to the **StandardAcpAdapter**. The `getProviderAdapter` function in [`src/core/acp/provider-adapter/index.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/provider-adapter/index.ts) normalizes the input string (handling case variations and hyphens) and checks against registered factories. If no match exists, it returns an instance of `StandardAcpAdapter`, which implements baseline ACP protocol handling, ensuring your application doesn't crash when encountering new or experimental providers.

### Where do I configure default models for each provider tier?

Default tier mappings are configured in the `PROVIDER_MODEL_TIERS` constant within [`src/core/acp/provider-registry.ts`](https://github.com/phodal/routa/blob/main/src/core/acp/provider-registry.ts). This object maps abstract tiers (`fast`, `balanced`, `smart`) to specific model identifiers for each provider (e.g., mapping `"smart"` to `"opus-4.6"` for Claude). You can extend these mappings or override them at runtime through the `ProviderCreateConfig` passed during factory registration.