How to Extend LLM Providers
This document provides step-by-step instructions for three common extension scenarios: adding a new preset template, adding a new API format, and configuring search capabilities for a provider.
File Locations
| File | Path |
|---|---|
| Provider templates | packages/desktop/app/shared/llm-config.ts |
| Provider presets | packages/desktop/app/shared/provider-presets.ts |
| ProviderManager | packages/desktop/app/main/services/capabilities/llm/config-service/ProviderManager.ts |
| URL Builder | packages/desktop/app/main/services/capabilities/llm/completion/url-builder.ts |
| Header Builder | packages/desktop/app/main/services/capabilities/llm/completion/header-builder.ts |
| Message Converter | packages/desktop/app/main/services/capabilities/llm/completion/message-converter.ts |
| DirectApiHandler | packages/desktop/app/main/services/capabilities/llm/completion/DirectApiHandler.ts |
| StreamHandler | packages/desktop/app/main/services/capabilities/llm/completion/StreamHandler.ts |
| CompletionService | packages/desktop/app/main/services/capabilities/llm/completion/CompletionService.ts |
| Types | packages/desktop/app/main/services/capabilities/llm/completion/types.ts |
| ProviderSearchInjector | packages/desktop/app/main/services/capabilities/llm/completion/ProviderSearchInjector.ts |
| NativeSearchInjector | packages/desktop/app/main/services/capabilities/llm/completion/NativeSearchInjector.ts |
| IPC Router Schemas | packages/desktop/app/main/services/routers/llm/schemas.ts |
| Frontend i18n | packages/renderer/src/locales/{en,zh,ja}/providers/llm.json |
Architectural Context
graph TB
subgraph ExtensionPoints ["Extension Points"]
direction TB
A["(1) New preset template<br/>provider-presets.ts"]
B["(2) New API format<br/>types + handlers"]
C["(3) Search config<br/>PROVIDER_SEARCH_CONFIGS"]
end
subgraph AffectedLayers ["Affected Layers"]
direction TB
Shared[shared layer<br/>types + presets]
Completion[completion layer<br/>Handlers + URL]
Config[config-service layer<br/>ProviderManager]
Router[router layer<br/>IPC Schemas]
Frontend[renderer layer<br/>UI + i18n]
end
A --> Shared
A --> Config
A --> Frontend
B --> Shared
B --> Completion
B --> Router
C --> Shared
C --> Completion
Scenario 1: Adding a New Preset Template
When you need to support a new LLM provider (such as a new Chinese cloud vendor or an international provider), create a preset template so users can add it from the UI in one click.
Data Structure
// Full interface for a preset template
interface PresetProviderTemplate {
id: string; // Unique ID (lowercase, e.g. 'minimax')
name: string; // Display name
apiFormat: ApiFormat; // API format
baseUrl: string; // Base URL
modelsEndpoint?: string; // Model discovery endpoint
models: string[]; // List of supported model IDs
modelConfigs: ModelConfig[]; // Per-model detailed config
features: string[]; // Feature tags
icon?: string; // Icon
website?: string; // Official website
docsUrl?: string; // API documentation
defaultSettings?: CompletionSettings; // Default completion parameters
}
Steps
Step 1: Define the Preset Template
Add an entry to the PROVIDER_PRESETS array in packages/desktop/app/shared/provider-presets.ts:
// Pseudocode — adding a new preset
{
id: 'newprovider',
name: 'NewProvider AI',
apiFormat: 'openai', // Most Chinese vendors are OpenAI-compatible
baseUrl: 'https://api.newprovider.com/v1',
modelsEndpoint: '/models',
models: ['np-large', 'np-lite', 'np-vision'],
modelConfigs: [
{
id: 'np-large',
name: 'NP Large',
enabled: true,
category: 'chat',
contextLength: 128000,
maxTokens: 8192,
vision: false,
functionCall: true,
reasoning: false,
},
{
id: 'np-lite',
name: 'NP Lite',
enabled: true,
category: 'chat',
contextLength: 32000,
maxTokens: 4096,
},
{
id: 'np-vision',
name: 'NP Vision',
enabled: true,
category: 'chat',
contextLength: 64000,
maxTokens: 4096,
vision: true,
},
],
features: ['chat', 'function_call'],
website: 'https://newprovider.com',
docsUrl: 'https://docs.newprovider.com/api',
}
Step 2 (Optional): Set as a Default Provider
If you want the new provider to appear in all users' initial lists:
Add the ID to the defaultTemplateIds array in ProviderManager.getDefaultProviders().
Also add the corresponding ProviderTemplate to PROVIDER_TEMPLATES in packages/desktop/app/shared/llm-config.ts.
Step 3 (Optional): Add Coding Plan Support
If the provider has a dedicated Coding Plan API:
// Add to CODING_PLAN_URL_PRESETS
CODING_PLAN_URL_PRESETS['newprovider'] = {
baseUrl: 'https://api.newprovider.com/coding/v1',
separateApiKey: false, // Whether a separate API Key is needed
};
Step 4 (Optional): Add Follow-Provider Model Mapping
If you want automatic selection of the same provider's background/vision models:
// Add to PROVIDER_MODEL_MAPPINGS
PROVIDER_MODEL_MAPPINGS['newprovider'] = {
primary: 'np-large',
background: 'np-lite',
vision: 'np-vision', // Set to null if no vision model exists
};
Step 5: Add Frontend i18n
Add the provider name translation to providers/llm.json under packages/renderer/src/locales/ for en/zh/ja.
Scenario 2: Adding a New API Format
When you encounter a provider API that is incompatible with existing formats (openai/anthropic/google/azure-openai/openai-response), you need to add a new API format.
Step Overview
flowchart TD
S1["(1) Define format identifier<br/>types.ts"] --> S2["(2) URL builder<br/>url-builder.ts"]
S2 --> S3["(3) Header builder<br/>header-builder.ts"]
S3 --> S4["(4) Message conversion<br/>message-converter.ts"]
S4 --> S5["(5) Non-streaming handler<br/>DirectApiHandler.ts"]
S5 --> S6["(6) Streaming handler<br/>StreamHandler.ts"]
S6 --> S7["(7) Register dispatch<br/>CompletionService.ts"]
S7 --> S8["(8) Schema update<br/>schemas.ts"]
Step 1: Define the Format Identifier
Add the new value to ApiFormat in packages/desktop/app/main/services/capabilities/llm/completion/types.ts:
// Before
type ApiFormat = 'openai' | 'anthropic' | 'google' | 'azure-openai' | 'openai-response';
// After
type ApiFormat = 'openai' | 'anthropic' | 'google' | 'azure-openai' | 'openai-response' | 'newformat';
Also update ApiFormatSchema in packages/desktop/app/main/services/capabilities/llm/config-service/schemas.ts:
const ApiFormatSchema = z.enum([
'openai', 'anthropic', 'google', 'azure-openai', 'openai-response', 'newformat'
]);
And update llmProviderCreateSchema in packages/desktop/app/main/services/routers/llm/schemas.ts.
Step 2: URL Builder
Add a builder function in url-builder.ts:
// Pseudocode
function buildNewFormatApiUrl(baseUrl: string): string {
// Build the correct endpoint URL according to the provider's API docs
// Handle various baseUrl input formats
}
Add recognition logic for the new format in resolveApiFormat() (if it needs to be inferred from apiType).
Add a branch for the new format in buildProviderApiUrl().
Step 3: Header Builder
Handle authentication headers for the new format in getProviderHeaders() within header-builder.ts:
// Pseudocode — different providers use different auth schemes
// OpenAI: Authorization: Bearer <key>
// Anthropic: x-api-key: <key>
// New format may use different headers
Step 4: Message Format Conversion
Add a conversion function in message-converter.ts:
// Pseudocode
function convertMessageToNewFormat(message: SimpleChatMessage): NewFormatMessage {
// Convert the generic message format to the provider-specific format
// Handle role mapping, content structure, images, tool calls, etc.
}
Step 5: Non-Streaming Handler
Add to DirectApiHandler.ts:
// Pseudocode
async function callNewFormatCompletion(
provider: LLMProvider,
apiKey: string,
options: CompletionOptions,
logger: LoggerService
): Promise<CompletionResult> {
// Build request body
// Send request
// Parse response into CompletionResult
}
Step 6: Streaming Handler
Add to StreamHandler.ts:
// Pseudocode
async function streamNewFormatCompletion(
provider: LLMProvider,
apiKey: string,
options: CompletionOptions,
messageId: string,
callbacks: StreamCallbacks,
logger: LoggerService
): Promise<void> {
// Build streaming request body
// Use streamSSEResponse() or custom stream parsing
// Call callbacks to deliver incremental content
}
Step 7: Register Dispatch
Register the new format in CompletionService:
// In the switch inside callDirectHandler
case 'newformat':
return callNewFormatCompletion(provider, apiKey, options, this.logger);
// In the switch inside callStreamHandler
case 'newformat':
await streamNewFormatCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
return;
Step 8: Schema Update
Ensure all Zod schemas that reference the ApiFormat enum are updated:
capabilities/llm/config-service/schemas.ts→ApiFormatSchemarouters/llm/schemas.ts→llmProviderCreateSchema.apiFormat
Scenario 3: Adding Search Configuration for a Provider
When a provider supports web search, you need to configure the search injection method.
Search Type Decision
flowchart TD
Start[Provider supports search?] --> Type{Search implementation}
Type -->|Request parameter| ModelParam["model-param<br/>modify request body"]
Type -->|Built-in tool definition| BuiltinTool["builtin-tool<br/>inject into tools array"]
Type -->|MCP server| MCP["mcp<br/>external handling"]
Type -->|SDK native| SDKNative["sdk-native<br/>NativeSearchInjector"]
Type -->|Not supported| None["none<br/>no config needed"]
Steps
Step 1: Determine the Search Type
| Search implementation | Criteria | Examples |
|---|---|---|
model-param | API enables search via a request parameter (e.g. enable_search: true) | DashScope, Baidu |
builtin-tool | API requires injecting a specific tool definition into the tools array | Kimi, Volcengine |
mcp | Search is provided by an external MCP server | Custom deployment |
sdk-native | SDK handles search natively (e.g. Anthropic server-side tools) | Anthropic |
none | Search not supported | Ollama |
Step 2: Add the Search Config
Add an entry to PROVIDER_SEARCH_CONFIGS in packages/desktop/app/shared/provider-presets.ts:
model-param type (request parameter):
// Pseudocode
PROVIDER_SEARCH_CONFIGS['newprovider'] = {
type: 'model-param',
paramName: 'enable_search', // parameter name
paramValue: true, // parameter value
extraParams: { // extra parameters (optional)
search_mode: 'auto',
},
applicableModels: null, // null means all models
};
builtin-tool type (built-in tool):
// Pseudocode
PROVIDER_SEARCH_CONFIGS['newprovider'] = {
type: 'builtin-tool',
toolDefinition: {
type: 'function',
function: {
name: 'web_search',
description: 'Search the web for information',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
},
required: ['query'],
},
},
},
conflictsWithFC: false, // Whether it conflicts with function calling
applicableModels: ['np-large'], // Only certain models support it (null = all)
};
Step 3: Verify Injection
ProviderSearchInjector automatically detects a provider's search config via getSearchConfig() and builds the injection content in buildSearchAugmentation(). No additional code changes are required.
File Modification Checklist
Scenario 1: New Preset Template
| File | Change | Required |
|---|---|---|
shared/provider-presets.ts | Add PROVIDER_PRESETS entry | Yes |
shared/llm-config.ts | Add PROVIDER_TEMPLATES entry (if needed as default) | Optional |
capabilities/llm/config-service/ProviderManager.ts | Add to defaultTemplateIds (if needed as default) | Optional |
shared/provider-presets.ts | CODING_PLAN_URL_PRESETS (if needed) | Optional |
shared/provider-presets.ts | PROVIDER_MODEL_MAPPINGS (if needed) | Optional |
shared/provider-presets.ts | PROVIDER_SEARCH_CONFIGS (if search needed) | Optional |
| i18n JSON files (en/zh/ja) | Provider name translations | Recommended |
Scenario 2: New API Format
| File | Change | Required |
|---|---|---|
capabilities/llm/completion/types.ts | ApiFormat type | Yes |
capabilities/llm/completion/url-builder.ts | URL builder + resolveApiFormat + buildProviderApiUrl | Yes |
capabilities/llm/completion/header-builder.ts | Auth headers | Yes |
capabilities/llm/completion/message-converter.ts | Message format conversion | Yes |
capabilities/llm/completion/DirectApiHandler.ts | Non-streaming handler | Yes |
capabilities/llm/completion/StreamHandler.ts | Streaming handler | Yes |
capabilities/llm/completion/CompletionService.ts | Switch branch registration | Yes |
capabilities/llm/config-service/schemas.ts | ApiFormatSchema | Yes |
routers/llm/schemas.ts | llmProviderCreateSchema | Yes |
Scenario 3: Add Search Config
| File | Change | Required |
|---|---|---|
shared/provider-presets.ts | PROVIDER_SEARCH_CONFIGS entry | Yes |
Testing Guide
Preset Template Tests
-
Unit tests: Verify template data integrity
- All required fields are present and valid
- Every model ID in modelConfigs exists in the models array
- apiFormat value is within the ApiFormat enum
-
Integration tests:
- Create a provider from preset → verify provider data is correct
- Enable provider → configure a valid API Key → send a test message
- Model discovery → verify returned model list
-
UI tests:
- New preset appears in the preset list on the settings page
- Clicking "Add" correctly creates the provider
- Provider config page displays the correct fields
API Format Tests
-
URL builder tests:
- Various baseUrl input formats → correct API endpoint
- URLs with path prefixes → prefix not lost
-
Message conversion tests:
- Plain text message → correct format
- Message with images → correctly handled
- Message with tool calls → correct format
-
Handler tests:
- Non-streaming: send request → parse response → CompletionResult
- Streaming: SSE events → callbacks triggered correctly
- Error handling: network errors, API errors, format errors
-
End-to-end tests:
- Use
testModel()to verify the full pipeline - Streaming conversation → verify onDelta/onDone callbacks
- Use
Search Config Tests
-
Injection tests:
getSearchConfig()correctly recognizes the new providerbuildSearchAugmentation()generates correct injection content- model-param type: request body contains correct parameters
- builtin-tool type: tools array contains the correct tool definition
-
Conflict tests:
- When
conflictsWithFCis true: search tool and function calling do not coexist applicableModelsrestriction: search is not injected for non-applicable models
- When
Related Files
| File | Relationship |
|---|---|
shared/llm-config.ts | PROVIDER_TEMPLATES, ApiFormat, core types |
shared/provider-presets.ts | Preset templates, search configs, Coding Plan URLs, model mappings |
capabilities/llm/config-service/ProviderManager.ts | Default provider list, template creation logic |
capabilities/llm/config-service/schemas.ts | ApiFormatSchema, validation schemas |
capabilities/llm/completion/types.ts | ApiFormat type definition |
capabilities/llm/completion/url-builder.ts | resolveApiFormat, URL builders |
capabilities/llm/completion/header-builder.ts | Auth headers |
capabilities/llm/completion/message-converter.ts | Message format conversion |
capabilities/llm/completion/DirectApiHandler.ts | Non-streaming API handler |
capabilities/llm/completion/StreamHandler.ts | Streaming API handler |
capabilities/llm/completion/CompletionService.ts | Handler dispatch registration |
capabilities/llm/completion/ProviderSearchInjector.ts | Search injection logic |
routers/llm/schemas.ts | IPC parameter validation schemas |
| i18n JSON files | Frontend translations |