# 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 one of:
- `ANTHROPIC_API_KEY` environment variable
- `CLAUDE_CODE_OAUTH_TOKEN` 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 (one of):
ANTHROPIC_API_KEY=sk-ant-...
CLAUDE_CODE_OAUTH_TOKEN=...
```

### 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! ✅
