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:

  1. Resolve the connection based on session, workspace default, or global default
  2. Map the provider type to an internal AgentProvider identifier
  3. Build the backend context bundling connection, auth, and capabilities
  4. Instantiate the concrete backend (ClaudeAgent or PiAgent)
  5. 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() or runChat() without knowing whether Claude or Pi is handling the request.
  • Provider resolution maps LlmProviderType values like 'anthropic' or 'pi' to concrete SDK implementations (ClaudeAgent or PiAgent) via providerTypeToAgentProvider().
  • 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 a ProviderDriver, and registering it in the factory's DRIVER_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:

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 →