# How the LLM Adapter System Works in Prompt-Optimizer and How to Add a New Provider

> Learn how Prompt-Optimizer's LLM adapter system works and easily add new LLM providers by creating adapters. Integrate any service with just four methods.

- Repository: [且炼时光/prompt-optimizer](https://github.com/linshenkx/prompt-optimizer)
- Tags: internals
- Published: 2026-02-23

---

**Prompt-Optimizer uses a provider-adapter pattern that lets you add any new LLM service by creating a concrete adapter class, registering it in the central registry, and implementing four required methods.**

The **LLM adapter system** in [linshenkx/prompt-optimizer](https://github.com/linshenkx/prompt-optimizer) abstracts all model providers behind a unified interface located in `packages/core/src/services/llm/adapters/`. This architecture allows the UI and core services to interact with OpenAI, Google Gemini, Anthropic Claude, or any custom endpoint through a single consistent API, making it straightforward to **add a new provider** without modifying business logic elsewhere in the codebase.

## Understanding the LLM Adapter Architecture

### The Abstract Base Class

All adapters inherit from `AbstractTextProviderAdapter` defined in [`abstract-adapter.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/abstract-adapter.ts). This base class establishes the contract that every provider must fulfill through four abstract methods:

- **`getProvider(): ProviderMeta`** – Returns static metadata including the provider ID, display name, and default base URL.
- **`getModels(): ModelMeta[]`** – Returns the catalogue of supported models with token limits and capabilities.
- **`buildDefaultModel(modelId: string): ModelMeta`** – Constructs a fallback model definition when the requested model ID is not in the static list.
- **`sendMessage(messages: Message[], config: SendMessageConfig): Promise<LLMResponse>`** – Executes the HTTP request to the provider’s chat completions endpoint and normalizes the response.

Source: [`packages/core/src/services/llm/adapters/abstract-adapter.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/core/src/services/llm/adapters/abstract-adapter.ts) contains the interface definition and abstract implementation.

### Concrete Adapter Implementations

Each supported LLM ships its own adapter file that extends the base class and specializes the request format. Reference implementations include:

- **[`openai-adapter.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/openai-adapter.ts)** – Handles OpenAI-compatible REST APIs.
- **[`gemini-adapter.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/gemini-adapter.ts)** – Manages Google Gemini-specific authentication and request shaping.
- **[`anthropic-adapter.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/anthropic-adapter.ts)** – Implements Anthropic’s Claude API requirements.

These files reside in `packages/core/src/services/llm/adapters/` and demonstrate how to map provider-specific payloads to the common `LLMResponse` shape expected by the application.

### The TextAdapterRegistry

The `TextAdapterRegistry` in [`registry.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/registry.ts) maintains a `Map<string, ITextProviderAdapter>` that resolves provider IDs to adapter instances. At startup, the registry populates this map with built-in adapters:

```typescript
this.adapters = new Map();
this.adapters.set('openai', new OpenAIAdapter());
this.adapters.set('gemini', new GeminiAdapter());
this.adapters.set('anthropic', new AnthropicAdapter());

```

Downstream code calls `TextAdapterRegistry.getAdapter(providerId)` to retrieve the appropriate implementation, enabling polymorphic access to any LLM service without hardcoding provider-specific logic.

## How to Add a New LLM Provider to Prompt-Optimizer

### Step 1 – Create the Adapter Class

Create a new file in `packages/core/src/services/llm/adapters/` (e.g., [`myprovider-adapter.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/myprovider-adapter.ts)) and extend `AbstractTextProviderAdapter`:

```typescript
import { AbstractTextProviderAdapter } from './abstract-adapter';
import type { ProviderMeta, ModelMeta, Message, SendMessageConfig, LLMResponse } from '../types';

export class MyProviderAdapter extends AbstractTextProviderAdapter {
  getProvider(): ProviderMeta {
    return {
      id: 'myprovider',
      name: 'MyProvider',
      defaultBaseURL: 'https://api.myprovider.com/v1',
    };
  }

  getModels(): ModelMeta[] {
    return [
      { id: 'model-lite', name: 'Lite', maxTokens: 2048 },
      { id: 'model-pro', name: 'Pro', maxTokens: 8192 },
    ];
  }

  buildDefaultModel(modelId: string): ModelMeta {
    const found = this.getModels().find(m => m.id === modelId);
    return found ?? { id: modelId, name: modelId, maxTokens: 4096 };
  }

  async sendMessage(messages: Message[], cfg: SendMessageConfig): Promise<LLMResponse> {
    const url = `${this.getProvider().defaultBaseURL}/chat/completions`;
    const body = {
      model: cfg.modelId,
      messages,
      temperature: cfg.temperature,
    };
    
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${cfg.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    
    const data = await response.json();
    
    return {
      id: data.id,
      choices: data.choices.map((c: any) => ({
        message: c.message,
        finish_reason: c.finish_reason,
      })),
    };
  }
}

```

**Key implementation details:**
- Maintain the exact method signatures defined in the abstract class to ensure type safety.
- Normalize the provider’s response inside `sendMessage` to match the `LLMResponse` interface expected by the UI.
- Handle authentication by reading `cfg.apiKey` passed from user settings.

### Step 2 – Register in the Adapter Registry

Import and instantiate your adapter in [`registry.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/registry.ts):

```typescript
import { MyProviderAdapter } from './myprovider-adapter';

// Inside the registry constructor or initialization block:
this.adapters.set('myprovider', new MyProviderAdapter());

```

Once registered, `TextAdapterRegistry.getAdapter('myprovider')` returns your implementation throughout the application. The UI layer (via `useModelManager` in [`packages/ui/src/composables/model/useModelManager.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/ui/src/composables/model/useModelManager.ts)) automatically detects the new provider and includes it in model selection dropdowns.

### Step 3 – Test Your Implementation

Add unit tests in [`packages/core/tests/unit/llm/myprovider-adapter.test.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/core/tests/unit/llm/myprovider-adapter.test.ts) to verify contract compliance:

```typescript
import { describe, it, expect, vi } from 'vitest';
import { TextAdapterRegistry } from '@/services/llm/adapters/registry';

describe('MyProviderAdapter', () => {
  it('returns correct provider metadata', () => {
    const adapter = TextAdapterRegistry.getAdapter('myprovider');
    const meta = adapter.getProvider();
    
    expect(meta.id).toBe('myprovider');
    expect(meta.name).toBe('MyProvider');
  });

  it('handles sendMessage correctly', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({
        id: 'resp-123',
        choices: [{ message: { role: 'assistant', content: 'Hello' }, finish_reason: 'stop' }]
      })
    });
    
    const adapter = TextAdapterRegistry.getAdapter('myprovider');
    const result = await adapter.sendMessage(
      [{ role: 'user', content: 'Hi' }],
      { modelId: 'model-pro', apiKey: 'test-key', stream: false }
    );
    
    expect(result.choices[0].message.content).toBe('Hello');
  });
});

```

Run `pnpm test` to validate that your adapter integrates correctly with the existing test suite.

## Complete Usage Example

After completing the three steps above, consume the new provider through the registry abstraction:

```typescript
import { TextAdapterRegistry } from '@/services/llm/adapters/registry';

// Resolve the adapter
const adapter = TextAdapterRegistry.getAdapter('myprovider');

// List available models for configuration UI
const models = adapter.getModels();
// → [{ id: 'model-lite', name: 'Lite', maxTokens: 2048 }, ...]

// Execute a chat completion
const response = await adapter.sendMessage(
  [{ role: 'user', content: 'Optimize this prompt for clarity.' }],
  {
    modelId: 'model-pro',
    apiKey: process.env.MYPROVIDER_API_KEY,
    stream: false,
    temperature: 0.7,
  }
);

console.log(response.choices[0].message.content);

```

This pattern ensures that the rest of Prompt-Optimizer—including the web UI, desktop app, and browser extension—can use your new provider without any additional code changes.

## Summary

- **Abstract Base:** All adapters extend `AbstractTextProviderAdapter` in [`abstract-adapter.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/abstract-adapter.ts) and implement four required methods: `getProvider()`, `getModels()`, `buildDefaultModel()`, and `sendMessage()`.
- **Registry Pattern:** The `TextAdapterRegistry` in [`registry.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/registry.ts) maps provider IDs to adapter instances, enabling dynamic resolution across the application.
- **Three-Step Integration:** To add a new provider, create an adapter class, register it in the registry, and add unit tests following existing patterns in `packages/core/tests/unit/llm/`.
- **UI Automation:** Once registered, the provider automatically appears in model selection dropdowns via `useModelManager`, requiring no manual UI updates.

## Frequently Asked Questions

### What is the minimum set of methods I must implement to create a working adapter?

You must implement all four abstract methods defined in `AbstractTextProviderAdapter`: `getProvider()` to return metadata, `getModels()` to list available models, `buildDefaultModel()` to handle unknown model IDs, and `sendMessage()` to execute the API request. Missing any of these will trigger TypeScript compilation errors because the class is abstract.

### Do I need to modify the UI code when adding a new provider?

No. The UI consumes adapters through the `TextAdapterRegistry` and the `useModelManager` composable. As long as you register your adapter in [`registry.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/registry.ts) with a unique ID, the provider automatically appears in configuration screens and model dropdowns. You only need UI changes if you want to add custom icons or provider-specific configuration panels.

### How does the registry handle provider selection at runtime?

The `TextAdapterRegistry` maintains an internal `Map<string, ITextProviderAdapter>`. When a user selects a provider in the settings, the application calls `registry.getAdapter(providerId)` to retrieve the instance. This instance is then used for all subsequent operations, including fetching model lists in [`packages/ui/src/composables/model/useModelManager.ts`](https://github.com/linshenkx/prompt-optimizer/blob/main/packages/ui/src/composables/model/useModelManager.ts) and executing prompts in the core service layer.

### Can I support streaming responses in my custom adapter?

Yes. The `SendMessageConfig` parameter passed to `sendMessage()` includes a `stream` boolean. When `stream` is true, your adapter should return a `ReadableStream` or handle Server-Sent Events (SSE) according to your provider’s API. Ensure you normalize the streaming chunks into the `LLMResponse` format expected by the UI, or adjust the UI consuming code to handle stream-specific return types if you extend the base interface.