Skip to content

feat: LLM provider abstraction — add Anthropic Claude & Ollama (local) support #281

@d-oit

Description

@d-oit

Problem

Only openrouter.ts and kilo.ts are implemented in src/lib/llm/. The PROVIDER_MODELS map is hardcoded in index.ts. There is no way to add Anthropic Claude or a local Ollama endpoint without modifying core provider files. This makes the system brittle and not extensible.

Current State

src/lib/llm/
  index.ts          # hardcoded PROVIDER_MODELS map
  openrouter.ts     # OpenRouter provider
  kilo.ts           # KiloCode provider
  types.ts          # basic types

No ILLMProvider interface exists — providers are called directly without abstraction.

Proposed Implementation

1. Define ILLMProvider interface

// src/lib/llm/types.ts
export interface ILLMProvider {
  id: string;
  name: string;
  chat(messages: Message[], options: ChatOptions): Promise<ChatResponse>;
  stream?(messages: Message[], options: ChatOptions): AsyncIterable<string>;
  listModels?(): Promise<ModelInfo[]>;
  validate?(apiKey: string): Promise<boolean>;
}

export interface ChatOptions {
  model: string;
  temperature?: number;
  maxTokens?: number;
  systemPrompt?: string;
  tools?: ToolDefinition[];
}

2. Provider registry

// src/lib/llm/registry.ts
const providers = new Map<string, ILLMProvider>();

export function registerProvider(provider: ILLMProvider) {
  providers.set(provider.id, provider);
}

export function getProvider(id: string): ILLMProvider {
  const p = providers.get(id);
  if (!p) throw new Error(`Unknown LLM provider: ${id}`);
  return p;
}

export function listProviders(): ILLMProvider[] {
  return Array.from(providers.values());
}

3. Anthropic Claude adapter

// src/lib/llm/anthropic.ts
import Anthropic from '@anthropic-ai/sdk';

export class AnthropicProvider implements ILLMProvider {
  id = 'anthropic';
  name = 'Anthropic Claude';
  
  async chat(messages: Message[], opts: ChatOptions): Promise<ChatResponse> {
    const client = new Anthropic({ apiKey: opts.apiKey });
    const response = await client.messages.create({
      model: opts.model,
      max_tokens: opts.maxTokens ?? 4096,
      messages: messages.filter(m => m.role !== 'system').map(m => ({
        role: m.role as 'user' | 'assistant',
        content: m.content
      })),
      system: messages.find(m => m.role === 'system')?.content,
    });
    return { content: response.content[0].type === 'text' ? response.content[0].text : '' };
  }
}

4. Ollama local adapter

// src/lib/llm/ollama.ts
export class OllamaProvider implements ILLMProvider {
  id = 'ollama';
  name = 'Ollama (Local)';
  
  constructor(private baseURL = 'http://localhost:11434') {}
  
  async chat(messages: Message[], opts: ChatOptions): Promise<ChatResponse> {
    const res = await fetch(`${this.baseURL}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        model: opts.model,
        messages,
        stream: false,
      }),
    });
    const data = await res.json();
    return { content: data.message.content };
  }
  
  async listModels(): Promise<ModelInfo[]> {
    const res = await fetch(`${this.baseURL}/api/tags`);
    const data = await res.json();
    return data.models.map((m: any) => ({ id: m.name, name: m.name }));
  }
}

5. Settings UI additions

  • Add Provider dropdown in AI settings with: OpenRouter, KiloCode, Anthropic, Ollama
  • Ollama: show baseURL input field (default: http://localhost:11434)
  • Auto-detect available Ollama models via listModels() call

Acceptance Criteria

  • ILLMProvider interface defined in types.ts
  • ProviderRegistry implemented with register/get/list functions
  • AnthropicProvider implemented and unit tested with mock
  • OllamaProvider implemented and unit tested with mock HTTP server
  • Provider selection UI in settings
  • baseURL override support for custom OpenAI-compatible endpoints
  • Existing OpenRouter and KiloCode providers refactored to implement ILLMProvider
  • No regression in existing provider tests

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions