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 providerparseCompoundModelId– Parsesprovider:modelcompound strings used throughout the UI and backendfindBestProviderForRole– 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:
- Create the adapter class implementing
BaseProviderAdapterwith appropriateimmediateToolInputsettings - Register the factory in the provider registry during server initialization
- Update
PROVIDER_MODEL_TIERSto include default model mappings for the new provider - 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()insrc/core/acp/provider-adapter/index.tshandles case-insensitive provider ID lookup and caching - Model Tiers (
fast,balanced,smart) automatically resolve to provider-specific models throughresolveModelForSpecialist - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →