Skip to Content
AdaptersAdapters

Adapters

The adapter layer is a single AgentAdapter interface that sits between the control plane and every LLM backend. The control plane dispatches a task and receives a result — it never knows or cares whether the work ran inside Claude Code CLI, an Ollama server, an OpenAI REST call, or a customer’s private GPU cluster. Adding a new provider means writing one adapter class; the control plane needs no changes.


Unified Interface [Live]

Each adapter is a package that exports a single execute() function:

// packages/adapters/<name>/src/server/execute.ts export type ProgressEvent = | { type: "tool_use"; toolName: string } | { type: "text"; preview: string }; export async function execute( ctx: AdapterExecutionContext, onProgress?: (event: ProgressEvent) => void, ): Promise<AdapterExecutionResult> { ... }
interface AdapterExecutionContext { config: AdapterConfig; // { cwd, model, timeoutSec, graceSec, ... } prompt: string; sessionId?: string; // resume prior session (Claude only) skillsDir?: string; // temp dir with skills files (Claude only) agentId: string; tenantId: string; runId: string; agentName: string; tenantName: string; } interface AdapterExecutionResult { success: boolean; output?: string; error?: string; sessionId?: string; costUsd?: number; durationMs?: number; }

Adapter Factory [Live]

AgentConfig in the DB stores the adapter type and model per agent role. Workers dynamically require the correct adapter package at runtime — no class hierarchy, no DI container:

// packages/agents/src/workers/setup.worker.ts type AdapterExecuteFn = typeof import("@leadmetrics/adapter-claude-local/server").execute; async function getAdapter(adapterType: string): Promise<AdapterExecuteFn> { if (adapterType === "codex_local") { const mod = require("@leadmetrics/adapter-codex-local/server") as { execute: AdapterExecuteFn }; return mod.execute; } if (adapterType === "gemini_local") { const mod = require("@leadmetrics/adapter-gemini-local/server") as { execute: AdapterExecuteFn }; return mod.execute; } // default: claude_local const mod = require("@leadmetrics/adapter-claude-local/server") as { execute: AdapterExecuteFn }; return mod.execute; }

Adapter-specific config differs by type:

const isCodex = adapterType === "codex_local"; const isGemini = adapterType === "gemini_local"; const config = isCodex ? { cwd, model, dangerouslyBypassApprovalsAndSandbox: true, timeoutSec: 900 } : isGemini ? { cwd, model, yolo: true, timeoutSec: 900 } : { cwd, model, dangerouslySkipPermissions: true, timeoutSec: 900 }; // Skills injected only for Claude (Codex + Gemini don't support --add-dir) const skillsDir = isCodex || isGemini ? undefined : await createSkillsDir(tenantId, agentRole);

Event Publishing & Run Tracking [Live]

Workers publish real-time events via Redis pub/sub. The API server subscribes and re-emits to the correct tenant room via Socket.IO:

// packages/agents/src/agent-events.ts await publishAgentEvent({ type: "agent:started", adapter: adapterType, // "claude_local" | "codex_local" | "gemini_local" model, tenantId, agentRole, runId, skills, ... });
Event typeWhat triggers itDB side effect
agent:startedBefore execute() is calledCreates AgentRun row (status: running)
agent:progressEach tool call inside the subprocessUpdates Redis live-progress key (TTL 30 min)
agent:completedAfter execute() resolvesUpdates AgentRun (status: completed, costUsd, durationMs)
agent:failedOn error or timeoutUpdates AgentRun (status: failed, error)

The AgentRun model stores adapter and model fields so the Manage portal can show per-adapter cost breakdowns.


Fallback & Resilience [To Build]

Planned: each adapter will have a configurable fallback provider. If the primary fails (outage, rate limit, auth error) the layer retries with a fallback automatically. Enterprise tenants on local_only mode cannot fall back to cloud providers. Not yet implemented.


Model Allocation by Task Type

TaskDefault providerDefault modelAlternative
Campaign strategy, decompositionClaudeSonnet 4.6GPT-4o
Long-form copywritingClaudeSonnet 4.6GPT-4o
Ad copy (Google, Meta)ClaudeSonnet 4.6GPT-4o
Email copyClaudeSonnet 4.6GPT-4o
SEO content briefClaudeSonnet 4.6GPT-4o
Social media postsClaudeSonnet 4.6GPT-4o-mini
Performance reportClaudeSonnet 4.6GPT-4o
Code generation (scripts)OpenAIo1 / CodexCodex Local (gpt-5.4)
Agentic code tasks (ChatGPT account)Codex Localgpt-5.4gpt-5.4-mini
Long-context AI tasks (Gemini)Gemini Localauto (Gemini 3)gemini-2.5-pro
Research extraction / scrapingOllamagemma3:4b
Task classification / routingOllamagemma3:4bGPT-4o-mini
Session summarisationOllamagemma3:4bGPT-4o-mini
Privacy-sensitive tasksOllamaany local
Enterprise custom runtimeWebhook(tenant’s)

Cost Tracking Per Provider

Cost is recorded on the AgentRun DB model (costUsd field) after each run completes.

ProviderCost sourceCalculation
claude_localToken counts from NDJSON result eventMODEL_PRICING[model] × tokens
codex_localToken counts from turn.completed usage eventsMODEL_PRICING[model] × tokens (TODO: currently shows )
gemini_localToken counts from final stream eventMODEL_PRICING[model] × tokens (TODO: currently shows )
ollama [To Build]eval_count + prompt_eval_count$0.00 always
openai [To Build]usage field in final streaming chunkMODEL_PRICING[model] × tokens
webhook [To Build]Usage object returned in callback payloadMODEL_PRICING[model] × tokens if known

Pricing tables live inside each adapter package (e.g. packages/adapters/codex-local/src/server/execute.ts).


Adding a New Provider

  1. Create packages/adapters/<name>/src/server/execute.ts exporting execute(ctx, onProgress) matching AdapterExecuteFn
  2. Add a /server subpath export in the package’s package.json
  3. Add a branch to getAdapter() in packages/agents/src/workers/setup.worker.ts (and any other workers that need it)
  4. Add model pricing inside the new adapter’s execute.ts
  5. Add the adapter key to the AgentConfig.adapter enum in packages/db/prisma/schema.prisma (string field — no enum constraint)
  6. Add adapter dropdown option in the Manage portal agent edit page
  7. Write unit + integration tests for the new adapter

Supported Adapters

StatusKeyDocBackendWhen to use
[Live]claude_localclaude.mdClaude Code CLI subprocess (NDJSON on stdout)Default; high-value reasoning, copywriting, strategy, brand-sensitive tasks
[Live]codex_localcodex-local.mdOpenAI Codex CLI subprocess (NDJSON on stdout)Code-heavy tasks, GPT-5 family; client-researcher uses this by default
[Live]gemini_localgemini-local.mdGoogle Gemini CLI subprocess (NDJSON on stdout)Large-context runs (Gemini 2.5 Pro), cost-optimised Flash runs
[To Build]openaiopenai.mdOpenAI REST API (SSE streaming)Tenant-preferred OpenAI without local CLI
[To Build]ollamaollama.mdLocal Ollama REST API (NDJSON streaming)Zero-cost tasks, data-local tenants, offline/air-gapped
[To Build]webhookwebhook.mdAsync HTTP POST + phone-home callbackEnterprise internal runtimes, any HTTP-capable agent

© 2026 Leadmetrics — Internal use only