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

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 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. 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 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:

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 maintains a Map<string, ITextProviderAdapter> that resolves provider IDs to adapter instances. At startup, the registry populates this map with built-in adapters:

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) and extend AbstractTextProviderAdapter:

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:

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) 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 to verify contract compliance:

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:

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 and implement four required methods: getProvider(), getModels(), buildDefaultModel(), and sendMessage().
  • Registry Pattern: The TextAdapterRegistry in 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →