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

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. 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) implements an immediate-input protocol. When Claude emits a tool_call event, the input parameters arrive fully formed in the rawInput field.

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) handles deferred inputs. The initial tool_call event contains an empty rawInput object, with actual parameters arriving later via tool_call_update events.

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:

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) consumes the registry to populate available options:

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 automatically selects appropriate models when specialists request specific capability tiers:

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

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) 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 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) sets inputFinalized: true immediately upon receiving a tool_call event because the rawInput contains complete parameters. The OpenCode adapter (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 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →