Files
automaker/docs/server/providers.md
SuperComboGamer 8d578558ff style: fix formatting with Prettier
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 20:31:57 -05:00

801 lines
19 KiB
Markdown

# Provider Architecture Reference
This document describes the modular provider architecture in `apps/server/src/providers/` that enables support for multiple AI model providers (Claude SDK, OpenAI Codex CLI, and future providers like Cursor, OpenCode, etc.).
---
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [Provider Interface](#provider-interface)
3. [Available Providers](#available-providers)
4. [Provider Factory](#provider-factory)
5. [Adding New Providers](#adding-new-providers)
6. [Provider Types](#provider-types)
7. [Best Practices](#best-practices)
---
## Architecture Overview
The provider architecture separates AI model execution logic from business logic, enabling clean abstraction and easy extensibility.
### Architecture Diagram
```
┌─────────────────────────────────────────┐
│ AgentService / AutoModeService │
│ (No provider logic) │
└──────────────────┬──────────────────────┘
┌─────────▼──────────┐
│ ProviderFactory │ Model-based routing
│ (Routes by model) │ "gpt-*" → Codex
└─────────┬──────────┘ "claude-*" → Claude
┌────────────┴────────────┐
│ │
┌─────▼──────┐ ┌──────▼──────┐
│ Claude │ │ Codex │
│ Provider │ │ Provider │
│ (Agent SDK)│ │ (CLI Spawn) │
└────────────┘ └─────────────┘
```
### Key Benefits
-**Adding new providers**: Only 1 new file + 1 line in factory
-**Services remain clean**: No provider-specific logic
-**All providers implement same interface**: Consistent behavior
-**Model prefix determines provider**: Automatic routing
-**Easy to test**: Each provider can be tested independently
---
## Provider Interface
**Location**: `apps/server/src/providers/base-provider.ts`
All providers must extend `BaseProvider` and implement the required methods.
### BaseProvider Abstract Class
```typescript
export abstract class BaseProvider {
protected config: ProviderConfig;
constructor(config: ProviderConfig = {}) {
this.config = config;
}
/**
* Get provider name (e.g., "claude", "codex")
*/
abstract getName(): string;
/**
* Execute a query and stream responses
*/
abstract executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage>;
/**
* Detect provider installation status
*/
abstract detectInstallation(): Promise<InstallationStatus>;
/**
* Get available models for this provider
*/
abstract getAvailableModels(): ModelDefinition[];
/**
* Check if provider supports a specific feature (optional)
*/
supportsFeature(feature: string): boolean {
return false;
}
}
```
### Shared Types
**Location**: `apps/server/src/providers/types.ts`
#### ExecuteOptions
Input configuration for executing queries:
```typescript
export interface ExecuteOptions {
prompt: string | Array<{ type: string; text?: string; source?: object }>;
model: string;
cwd: string;
systemPrompt?: string;
maxTurns?: number;
allowedTools?: string[];
mcpServers?: Record<string, unknown>;
abortController?: AbortController;
conversationHistory?: ConversationMessage[];
}
```
#### ProviderMessage
Output messages streamed from providers:
```typescript
export interface ProviderMessage {
type: 'assistant' | 'user' | 'error' | 'result';
subtype?: 'success' | 'error';
message?: {
role: 'user' | 'assistant';
content: ContentBlock[];
};
result?: string;
error?: string;
}
```
#### ContentBlock
Individual content blocks in messages:
```typescript
export interface ContentBlock {
type: 'text' | 'tool_use' | 'thinking' | 'tool_result';
text?: string;
thinking?: string;
name?: string;
input?: unknown;
tool_use_id?: string;
content?: string;
}
```
---
## Available Providers
### 1. Claude Provider (SDK-based)
**Location**: `apps/server/src/providers/claude-provider.ts`
Uses `@anthropic-ai/claude-agent-sdk` for direct SDK integration.
#### Features
- ✅ Native multi-turn conversation support
- ✅ Vision support (images)
- ✅ Tool use (Read, Write, Edit, Glob, Grep, Bash, WebSearch, WebFetch)
- ✅ Thinking blocks (extended thinking)
- ✅ Streaming responses
- ✅ No CLI installation required (npm dependency)
#### Model Detection
Routes models that:
- Start with `"claude-"` (e.g., `"claude-opus-4-5-20251101"`)
- Are Claude aliases: `"opus"`, `"sonnet"`, `"haiku"`
#### Authentication
Requires:
- `ANTHROPIC_API_KEY` environment variable
#### Example Usage
```typescript
const provider = new ClaudeProvider();
const stream = provider.executeQuery({
prompt: 'What is 2+2?',
model: 'claude-opus-4-5-20251101',
cwd: '/project/path',
systemPrompt: 'You are a helpful assistant.',
maxTurns: 20,
allowedTools: ['Read', 'Write', 'Bash'],
abortController: new AbortController(),
conversationHistory: [
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi! How can I help?' },
],
});
for await (const msg of stream) {
if (msg.type === 'assistant') {
console.log(msg.message?.content);
}
}
```
#### Conversation History Handling
Uses `convertHistoryToMessages()` utility to convert history to SDK format:
```typescript
const historyMessages = convertHistoryToMessages(conversationHistory);
for (const msg of historyMessages) {
yield msg; // Yield to SDK
}
```
---
### 2. Codex Provider (CLI-based)
**Location**: `apps/server/src/providers/codex-provider.ts`
Spawns OpenAI Codex CLI as a subprocess and converts JSONL output to provider format.
#### Features
- ✅ Subprocess execution (`codex exec --model <model> --json --full-auto`)
- ✅ JSONL stream parsing
- ✅ Supports GPT-5.1/5.2 Codex models
- ✅ Vision support (GPT-5.1, GPT-5.2)
- ✅ Tool use via MCP servers
- ✅ Timeout detection (30s no output)
- ✅ Abort signal handling
#### Model Detection
Routes models that:
- Start with `"gpt-"` (e.g., `"gpt-5.2"`, `"gpt-5.1-codex-max"`)
- Start with `"o"` (e.g., `"o1"`, `"o1-mini"`)
#### Available Models
| Model | Description | Context | Max Output | Vision |
| -------------------- | ------------------ | ------- | ---------- | ------ |
| `gpt-5.2` | Latest Codex model | 256K | 32K | Yes |
| `gpt-5.1-codex-max` | Maximum capability | 256K | 32K | Yes |
| `gpt-5.1-codex` | Standard Codex | 256K | 32K | Yes |
| `gpt-5.1-codex-mini` | Lightweight | 256K | 16K | No |
| `gpt-5.1` | General-purpose | 256K | 32K | Yes |
#### Authentication
Supports two methods:
1. **CLI login**: `codex login` (OAuth tokens stored in `~/.codex/auth.json`)
2. **API key**: `OPENAI_API_KEY` environment variable
#### Installation Detection
Uses `CodexCliDetector` to check:
- PATH for `codex` command
- npm global: `npm list -g @openai/codex`
- Homebrew (macOS): `/opt/homebrew/bin/codex`
- Common paths: `~/.local/bin/codex`, `/usr/local/bin/codex`
#### Example Usage
```typescript
const provider = new CodexProvider();
const stream = provider.executeQuery({
prompt: 'Fix the bug in main.ts',
model: 'gpt-5.2',
cwd: '/project/path',
systemPrompt: 'You are an expert TypeScript developer.',
abortController: new AbortController(),
});
for await (const msg of stream) {
if (msg.type === 'assistant') {
console.log(msg.message?.content);
} else if (msg.type === 'error') {
console.error(msg.error);
}
}
```
#### JSONL Event Conversion
Codex CLI outputs JSONL events that get converted to `ProviderMessage` format:
| Codex Event | Provider Message |
| ------------------------------------ | --------------------------------------------------------------------------------------- |
| `item.completed` (reasoning) | `{ type: "assistant", content: [{ type: "thinking" }] }` |
| `item.completed` (agent_message) | `{ type: "assistant", content: [{ type: "text" }] }` |
| `item.completed` (command_execution) | `{ type: "assistant", content: [{ type: "text", text: "```bash\n...\n```" }] }` |
| `item.started` (command_execution) | `{ type: "assistant", content: [{ type: "tool_use" }] }` |
| `item.updated` (todo_list) | `{ type: "assistant", content: [{ type: "text", text: "**Updated Todo List:**..." }] }` |
| `thread.completed` | `{ type: "result", subtype: "success" }` |
| `error` | `{ type: "error", error: "..." }` |
#### Conversation History Handling
Uses `formatHistoryAsText()` utility to prepend history as text context (CLI doesn't support native multi-turn):
```typescript
const historyText = formatHistoryAsText(conversationHistory);
combinedPrompt = `${historyText}Current request:\n${combinedPrompt}`;
```
#### MCP Server Configuration
**Location**: `apps/server/src/providers/codex-config-manager.ts`
Manages TOML configuration for MCP servers:
```typescript
await codexConfigManager.configureMcpServer(cwd, mcpServerScriptPath);
```
Generates `.codex/config.toml`:
```toml
[mcp_servers.automaker-tools]
command = "node"
args = ["/path/to/mcp-server.js"]
enabled_tools = ["UpdateFeatureStatus"]
```
---
## Provider Factory
**Location**: `apps/server/src/providers/provider-factory.ts`
Routes requests to the appropriate provider based on model string.
### Model-Based Routing
```typescript
export class ProviderFactory {
/**
* Get provider for a specific model
*/
static getProviderForModel(modelId: string): BaseProvider {
const lowerModel = modelId.toLowerCase();
// OpenAI/Codex models
if (lowerModel.startsWith('gpt-') || lowerModel.startsWith('o')) {
return new CodexProvider();
}
// Claude models
if (lowerModel.startsWith('claude-') || ['haiku', 'sonnet', 'opus'].includes(lowerModel)) {
return new ClaudeProvider();
}
// Default to Claude
return new ClaudeProvider();
}
/**
* Check installation status of all providers
*/
static async checkAllProviders(): Promise<Record<string, InstallationStatus>> {
const claude = new ClaudeProvider();
const codex = new CodexProvider();
return {
claude: await claude.detectInstallation(),
codex: await codex.detectInstallation(),
};
}
}
```
### Usage in Services
```typescript
import { ProviderFactory } from '../providers/provider-factory.js';
// In AgentService or AutoModeService
const provider = ProviderFactory.getProviderForModel(model);
const stream = provider.executeQuery(options);
for await (const msg of stream) {
// Handle messages (format is consistent across all providers)
}
```
---
## Adding New Providers
### Step 1: Create Provider File
Create `apps/server/src/providers/[name]-provider.ts`:
```typescript
import { BaseProvider } from './base-provider.js';
import type {
ExecuteOptions,
ProviderMessage,
InstallationStatus,
ModelDefinition,
} from './types.js';
export class CursorProvider extends BaseProvider {
getName(): string {
return 'cursor';
}
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
// Implementation here
// 1. Spawn cursor CLI or use SDK
// 2. Convert output to ProviderMessage format
// 3. Yield messages
}
async detectInstallation(): Promise<InstallationStatus> {
// Check if cursor is installed
// Return { installed: boolean, path?: string, version?: string }
}
getAvailableModels(): ModelDefinition[] {
return [
{
id: 'cursor-premium',
name: 'Cursor Premium',
modelString: 'cursor-premium',
provider: 'cursor',
description: "Cursor's premium model",
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
supportsTools: true,
tier: 'premium',
default: true,
},
];
}
supportsFeature(feature: string): boolean {
const supportedFeatures = ['tools', 'text', 'vision'];
return supportedFeatures.includes(feature);
}
}
```
### Step 2: Add Routing in Factory
Update `apps/server/src/providers/provider-factory.ts`:
```typescript
import { CursorProvider } from "./cursor-provider.js";
static getProviderForModel(modelId: string): BaseProvider {
const lowerModel = modelId.toLowerCase();
// Cursor models
if (lowerModel.startsWith("cursor-")) {
return new CursorProvider();
}
// ... existing routing
}
static async checkAllProviders() {
const cursor = new CursorProvider();
return {
claude: await claude.detectInstallation(),
codex: await codex.detectInstallation(),
cursor: await cursor.detectInstallation(), // NEW
};
}
```
### Step 3: Update Models List
Update `apps/server/src/routes/models.ts`:
```typescript
{
id: "cursor-premium",
name: "Cursor Premium",
provider: "cursor",
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
supportsTools: true,
}
```
### Step 4: Done!
No changes needed in:
- ✅ AgentService
- ✅ AutoModeService
- ✅ Any business logic
The provider architecture handles everything automatically.
---
## Provider Types
### SDK-Based Providers (like Claude)
**Characteristics**:
- Direct SDK/library integration
- No subprocess spawning
- Native multi-turn support
- Streaming via async generators
**Example**: ClaudeProvider using `@anthropic-ai/claude-agent-sdk`
**Advantages**:
- Lower latency
- More control over options
- Easier error handling
- No CLI installation required
---
### CLI-Based Providers (like Codex)
**Characteristics**:
- Subprocess spawning
- JSONL stream parsing
- Text-based conversation history
- Requires CLI installation
**Example**: CodexProvider using `codex exec --json`
**Advantages**:
- Access to CLI-only features
- No SDK dependency
- Can use any CLI tool
**Implementation Pattern**:
1. Use `spawnJSONLProcess()` from `subprocess-manager.ts`
2. Convert JSONL events to `ProviderMessage` format
3. Handle authentication (CLI login or API key)
4. Implement timeout detection
---
## Best Practices
### 1. Message Format Consistency
All providers MUST output the same `ProviderMessage` format so services can handle them uniformly:
```typescript
// ✅ Correct - Consistent format
yield {
type: "assistant",
message: {
role: "assistant",
content: [{ type: "text", text: "Response" }]
}
};
// ❌ Incorrect - Provider-specific format
yield {
customType: "response",
data: "Response"
};
```
### 2. Error Handling
Always yield error messages, never throw:
```typescript
// ✅ Correct
try {
// ...
} catch (error) {
yield {
type: "error",
error: (error as Error).message
};
return;
}
// ❌ Incorrect
throw new Error("Provider failed");
```
### 3. Abort Signal Support
Respect the abort controller:
```typescript
if (abortController?.signal.aborted) {
yield { type: "error", error: "Operation cancelled" };
return;
}
```
### 4. Conversation History
- **SDK providers**: Use `convertHistoryToMessages()` and yield messages
- **CLI providers**: Use `formatHistoryAsText()` and prepend to prompt
### 5. Image Handling
- **Vision models**: Pass images as content blocks
- **Non-vision models**: Extract text only using utilities
### 6. Logging
Use consistent logging prefixes:
```typescript
console.log(`[${this.getName()}Provider] Operation started`);
console.error(`[${this.getName()}Provider] Error:`, error);
```
### 7. Installation Detection
Implement thorough detection:
- Check multiple installation methods
- Verify authentication
- Return detailed status
### 8. Model Definitions
Provide accurate model metadata:
```typescript
{
id: "model-id",
name: "Human-readable name",
modelString: "exact-model-string",
provider: "provider-name",
description: "What this model is good for",
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
supportsTools: true,
tier: "premium" | "standard" | "basic",
default: false
}
```
---
## Testing Providers
### Unit Tests
Test each provider method independently:
```typescript
describe('ClaudeProvider', () => {
it('should detect installation', async () => {
const provider = new ClaudeProvider();
const status = await provider.detectInstallation();
expect(status.installed).toBe(true);
expect(status.method).toBe('sdk');
});
it('should stream messages correctly', async () => {
const provider = new ClaudeProvider();
const messages = [];
for await (const msg of provider.executeQuery(options)) {
messages.push(msg);
}
expect(messages.length).toBeGreaterThan(0);
expect(messages[0].type).toBe('assistant');
});
});
```
### Integration Tests
Test provider interaction with services:
```typescript
describe('Provider Integration', () => {
it('should work with AgentService', async () => {
const provider = ProviderFactory.getProviderForModel('claude-opus-4-5-20251101');
// Test full workflow
});
});
```
---
## Environment Variables
### Claude Provider
```bash
# Required:
ANTHROPIC_API_KEY=sk-ant-...
```
### Codex Provider
```bash
# Required (one of):
OPENAI_API_KEY=sk-...
# OR run: codex login
# Optional:
CODEX_CLI_PATH=/custom/path/to/codex
```
---
## Troubleshooting
### Provider Not Found
**Problem**: `ProviderFactory.getProviderForModel()` returns wrong provider
**Solution**: Check model string prefix in factory routing
### Authentication Errors
**Problem**: Provider fails with auth error
**Solution**:
1. Check environment variables
2. For CLI providers, verify CLI login status
3. Check `detectInstallation()` output
### JSONL Parsing Errors (CLI providers)
**Problem**: Failed to parse JSONL line
**Solution**:
1. Check CLI output format
2. Verify JSON is valid
3. Add error handling for malformed lines
### Timeout Issues (CLI providers)
**Problem**: Subprocess hangs
**Solution**:
1. Increase timeout in `spawnJSONLProcess` options
2. Check CLI process for hangs
3. Verify abort signal handling
---
## Future Provider Ideas
Potential providers to add:
1. **Cursor Provider** (`cursor-*`)
- CLI-based
- Code completion specialist
2. **OpenCode Provider** (`opencode-*`)
- SDK or CLI-based
- Open-source alternative
3. **Gemini Provider** (`gemini-*`)
- Google's AI models
- SDK-based via `@google/generative-ai`
4. **Ollama Provider** (`ollama-*`)
- Local model hosting
- CLI or HTTP API
Each would follow the same pattern:
1. Create `[name]-provider.ts` implementing `BaseProvider`
2. Add routing in `provider-factory.ts`
3. Update models list
4. Done! ✅