How Craft Agents Implements Multi-Provider LLM Routing: A Deep Dive into the Provider-Agnostic Backend
Craft Agents routes multi-provider LLM requests through a centralized factory that maps connection configurations to concrete SDK implementations, ensuring the rest of the codebase remains provider-agnostic.
The lukilabs/craft-agents-oss repository implements a sophisticated routing layer that abstracts every Large Language Model (LLM) service behind a unified interface. This architecture allows seamless switching between Anthropic, Pi (including OpenRouter and Bedrock), and future providers without changing application logic.
The Provider-Agnostic Architecture
At the core of Craft Agents' multi-provider LLM routing is the backend factory pattern. Instead of hardcoding provider-specific logic throughout the application, the system uses a centralized factory located in packages/shared/src/agent/backend/factory.ts to resolve connections and instantiate the appropriate SDK.
The routing layer treats every LLM service as an AgentBackend, whether it uses the native Anthropic SDK or the unified Pi SDK. This abstraction ensures that high-level features like completions, tool calls, and validation work identically regardless of the underlying provider.
The Routing Flow: From Connection to Concrete Backend
When a session initiates an LLM request, the system executes a five-step resolution pipeline:
- Resolve the connection based on session, workspace default, or global default
- Map the provider type to an internal AgentProvider identifier
- Build the backend context bundling connection, auth, and capabilities
- Instantiate the concrete backend (ClaudeAgent or PiAgent)
- Route all LLM calls through the common AgentBackend interface
Resolving the Connection
The resolveSessionConnection() function in packages/shared/src/agent/backend/factory.ts (lines 22-38) selects the appropriate LLM connection for the current session. It evaluates the session-specific override, falls back to the workspace default, and finally checks the global configuration.
Mapping Provider Types to Agent Providers
Once a connection is selected, providerTypeToAgentProvider() (lines 51-66) translates the storage-level LlmProviderType into the runtime AgentProvider identifier. This mapping normalizes provider strings like 'anthropic' or 'pi' into the internal SDK selections used by the factory.
Building the Backend Context
The resolveBackendContext() function (lines 53-78) aggregates the resolved connection, provider type, authentication method, model identifier, and capability flags into a ResolvedBackendContext object. This context serves as the immutable configuration passed to the backend constructor.
Instantiating the Concrete Backend
createBackendFromConnection() (lines 39-77) validates the provider-auth combination using isValidProviderAuthCombination() from packages/shared/src/config/llm-connections.ts, then delegates to createBackendFromResolvedContext() to instantiate either ClaudeAgent or PiAgent based on the resolved provider type.
Provider-Specific Implementations
Craft Agents currently supports two primary backend implementations, both satisfying the AgentBackend interface:
ClaudeAgent (packages/shared/src/agent/claude-agent.ts) wraps the official Anthropic SDK (@anthropic-ai/claude-agent-sdk). It handles native Anthropic authentication via environment variables (ANTHROPIC_API_KEY) or OAuth tokens (CLAUDE_CODE_OAUTH_TOKEN).
PiAgent (packages/shared/src/agent/pi-agent.ts) utilizes the unified Pi SDK (@mariozechner/pi-ai). This single backend handles multiple endpoints including the native Pi API, OpenRouter, and AWS Bedrock. The Pi driver in packages/shared/src/agent/backend/internal/drivers/pi.ts maps logical providers to physical endpoints—for example, routing openrouter to https://openrouter.ai/api/v1.
Validation and Safety Checks
Before instantiating any backend, the factory validates provider compatibility:
if (!isValidProviderAuthCombination(connection.providerType, connection.authType)) {
throw new Error(
`Invalid LLM connection configuration: provider '${connection.providerType}' ` +
`does not support auth type '${connection.authType}'.`
);
}
Source: packages/shared/src/agent/backend/factory.ts (lines 39-47)
Anthropic connections undergo additional validation via validateConnection(), which performs a lightweight SDK ping. Pi connections defer validation to the SDK, which verifies API keys on first use.
Code Examples
Creating a Backend from a Connection
The createBackendFromConnection() function resolves a stored connection slug into a working backend:
import { createBackendFromConnection } from '@/shared/agent/backend/factory.ts';
import { getDefaultBackendHostRuntime } from '@/shared/agent/backend/internal/runtime-resolver.ts';
// Assume a connection named "anthropic-api" exists in the config
const backend = createBackendFromConnection(
'anthropic-api',
{
workspace: { id: 'ws-1', name: 'Demo', slug: 'demo', rootPath: '/home/user/demo', createdAt: Date.now() },
session: { id: 'sess-1', workspaceRootPath: '/home/user/demo', createdAt: Date.now(), lastUsedAt: Date.now() },
isHeadless: false,
miniModel: 'claude-haiku-4-5',
},
getDefaultBackendHostRuntime()
);
// Use the common backend interface – the same call works for Claude or Pi
await backend.runMiniCompletion('Explain the routing logic in one sentence.');
Source: packages/shared/src/agent/backend/factory.ts (lines 39-77)
Validating a Connection
Use validateStoredBackendConnection() to verify credentials before initiating chat sessions:
import { validateStoredBackendConnection } from '@/shared/agent/backend/factory.ts';
import { getDefaultBackendHostRuntime } from '@/shared/agent/backend/internal/runtime-resolver.ts';
async function canConnect(slug: string): Promise<boolean> {
const result = await validateStoredBackendConnection({
slug,
hostRuntime: getDefaultBackendHostRuntime(),
});
return result.success;
}
// Usage
if (await canConnect('openrouter')) {
console.log('Connection is good!');
}
Source: packages/shared/src/agent/backend/factory.ts (lines 48-91)
Key Files in the Routing Layer
| File | Role | Link |
|---|---|---|
packages/shared/src/agent/backend/factory.ts |
Central factory that maps providerType → SDK, validates auth, builds backend contexts, and creates the concrete AgentBackend. |
factory.ts |
packages/shared/src/config/llm-connections.ts |
Defines LlmProviderType, LlmAuthType, and helper functions (isValidProviderAuthCombination). |
llm‑connections.ts |
packages/shared/src/agent/backend/internal/drivers/anthropic.ts |
Driver implementation for the Anthropic/Claude SDK (used by ClaudeAgent). |
anthropic.ts |
packages/shared/src/agent/backend/internal/drivers/pi.ts |
Driver implementation for the Pi unified API (used by PiAgent). Handles OpenRouter, Bedrock, etc. |
pi.ts |
packages/shared/src/config/provider-metadata.ts |
UI‑facing metadata (display names, dashboard URLs, icons) for each provider. | provider‑metadata.ts |
packages/shared/src/agent/claude-agent.ts |
Concrete backend for Anthropic – implements the AgentBackend interface. |
claude‑agent.ts |
packages/shared/src/agent/pi-agent.ts |
Concrete backend for Pi – implements the AgentBackend interface. |
pi‑agent.ts |
Summary
- Craft Agents implements multi-provider LLM routing through a centralized factory pattern in
packages/shared/src/agent/backend/factory.ts. - The AgentBackend interface abstracts all LLM operations, allowing high-level code to call
runMiniCompletion()orrunChat()without knowing whether Claude or Pi is handling the request. - Provider resolution maps
LlmProviderTypevalues like'anthropic'or'pi'to concrete SDK implementations (ClaudeAgentorPiAgent) viaproviderTypeToAgentProvider(). - Validation ensures auth type compatibility before instantiation, throwing descriptive errors for invalid provider/auth combinations.
- Extensibility is built-in: adding a new provider requires only defining a new
LlmProviderType, implementing aProviderDriver, and registering it in the factory'sDRIVER_REGISTRY.
Frequently Asked Questions
How does Craft Agents decide which LLM provider to use for a session?
The system calls resolveSessionConnection() in packages/shared/src/agent/backend/factory.ts (lines 22-38) to select a connection based on the current session's preference, falling back to the workspace default and then the global default if no override exists. Once resolved, the connection's providerType field determines whether the factory instantiates a ClaudeAgent or PiAgent.
What is the difference between ClaudeAgent and PiAgent in Craft Agents?
ClaudeAgent (packages/shared/src/agent/claude-agent.ts) is a thin wrapper around the official Anthropic SDK (@anthropic-ai/claude-agent-sdk) that handles native Claude models and Anthropic-specific authentication. PiAgent (packages/shared/src/agent/pi-agent.ts) uses the unified Pi SDK (@mariozechner/pi-ai) to interface with multiple endpoints including OpenRouter, AWS Bedrock, and the native Pi API, making it a multi-tenant backend for various cloud providers.
How does Craft Agents validate that an authentication method is compatible with a selected provider?
Before creating any backend, the factory calls isValidProviderAuthCombination() from packages/shared/src/config/llm-connections.ts to verify that the connection's authType (such as api_key or oauth) is supported by the specified providerType. If the combination is invalid, the factory throws an error immediately, preventing runtime authentication failures during actual LLM calls.
Can developers add new LLM providers to Craft Agents without modifying core application logic?
Yes, the architecture supports extensibility through the DRIVER_REGISTRY in packages/shared/src/agent/backend/factory.ts. To add a new provider, developers define a new LlmProviderType value in llm-connections.ts, implement a ProviderDriver interface in a new file under packages/shared/src/agent/backend/internal/drivers/, and register the driver in the factory. The existing routing logic automatically picks up the new provider because all LLM calls flow through the factory's createBackendFromConnection() method.
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 →