refactor provider 1
This commit is contained in:
@@ -54,6 +54,7 @@ import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
||||
import { applyProxySettings } from './proxy';
|
||||
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||
import { getRecentTokenUsageHistory } from '../utils/token-usage';
|
||||
import { getProviderService } from '../services/providers/provider-service';
|
||||
|
||||
/**
|
||||
* For custom/ollama providers, derive a unique key for OpenClaw config files
|
||||
@@ -995,6 +996,8 @@ function registerDeviceOAuthHandlers(mainWindow: BrowserWindow): void {
|
||||
* Provider-related IPC handlers
|
||||
*/
|
||||
function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
const providerService = getProviderService();
|
||||
|
||||
// Listen for OAuth success to automatically restart the Gateway with new tokens/configs.
|
||||
// Use a longer debounce (8s) so that provider:setDefault — which writes the full config
|
||||
// and then calls debouncedRestart(2s) — has time to fire and coalesce into a single
|
||||
@@ -1010,6 +1013,19 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
return await getAllProvidersWithKeyInfo();
|
||||
});
|
||||
|
||||
// New provider-service endpoints used by the account-based refactor.
|
||||
ipcMain.handle('provider:listVendors', async () => {
|
||||
return await providerService.listVendors();
|
||||
});
|
||||
|
||||
ipcMain.handle('provider:listAccounts', async () => {
|
||||
return await providerService.listAccounts();
|
||||
});
|
||||
|
||||
ipcMain.handle('provider:getAccount', async (_, accountId: string) => {
|
||||
return await providerService.getAccount(accountId);
|
||||
});
|
||||
|
||||
// Get a specific provider
|
||||
ipcMain.handle('provider:get', async (_, providerId: string) => {
|
||||
return await getProvider(providerId);
|
||||
|
||||
35
electron/services/providers/provider-migration.ts
Normal file
35
electron/services/providers/provider-migration.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { ProviderConfig } from '../../shared/providers/types';
|
||||
import {
|
||||
getDefaultProviderAccountId,
|
||||
providerConfigToAccount,
|
||||
saveProviderAccount,
|
||||
} from './provider-store';
|
||||
import { getClawXProviderStore } from './store-instance';
|
||||
|
||||
const PROVIDER_STORE_SCHEMA_VERSION = 1;
|
||||
|
||||
export async function ensureProviderStoreMigrated(): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const schemaVersion = Number(store.get('schemaVersion') ?? 0);
|
||||
|
||||
if (schemaVersion >= PROVIDER_STORE_SCHEMA_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
const legacyProviders = (store.get('providers') ?? {}) as Record<string, ProviderConfig>;
|
||||
const defaultProviderId = (store.get('defaultProvider') ?? null) as string | null;
|
||||
const existingDefaultAccountId = await getDefaultProviderAccountId();
|
||||
|
||||
for (const provider of Object.values(legacyProviders)) {
|
||||
const account = providerConfigToAccount(provider, {
|
||||
isDefault: provider.id === defaultProviderId,
|
||||
});
|
||||
await saveProviderAccount(account);
|
||||
}
|
||||
|
||||
if (!existingDefaultAccountId && defaultProviderId) {
|
||||
store.set('defaultProviderAccountId', defaultProviderId);
|
||||
}
|
||||
|
||||
store.set('schemaVersion', PROVIDER_STORE_SCHEMA_VERSION);
|
||||
}
|
||||
61
electron/services/providers/provider-service.ts
Normal file
61
electron/services/providers/provider-service.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
PROVIDER_DEFINITIONS,
|
||||
getProviderDefinition,
|
||||
} from '../../shared/providers/registry';
|
||||
import type {
|
||||
ProviderAccount,
|
||||
ProviderConfig,
|
||||
ProviderDefinition,
|
||||
} from '../../shared/providers/types';
|
||||
import { ensureProviderStoreMigrated } from './provider-migration';
|
||||
import {
|
||||
getDefaultProviderAccountId,
|
||||
getProviderAccount,
|
||||
listProviderAccounts,
|
||||
providerConfigToAccount,
|
||||
saveProviderAccount,
|
||||
setDefaultProviderAccount,
|
||||
} from './provider-store';
|
||||
|
||||
export class ProviderService {
|
||||
async listVendors(): Promise<ProviderDefinition[]> {
|
||||
return PROVIDER_DEFINITIONS;
|
||||
}
|
||||
|
||||
async listAccounts(): Promise<ProviderAccount[]> {
|
||||
await ensureProviderStoreMigrated();
|
||||
return listProviderAccounts();
|
||||
}
|
||||
|
||||
async getAccount(accountId: string): Promise<ProviderAccount | null> {
|
||||
await ensureProviderStoreMigrated();
|
||||
return getProviderAccount(accountId);
|
||||
}
|
||||
|
||||
async getDefaultAccountId(): Promise<string | undefined> {
|
||||
await ensureProviderStoreMigrated();
|
||||
return getDefaultProviderAccountId();
|
||||
}
|
||||
|
||||
async syncLegacyProvider(config: ProviderConfig, options?: { isDefault?: boolean }): Promise<ProviderAccount> {
|
||||
await ensureProviderStoreMigrated();
|
||||
const account = providerConfigToAccount(config, options);
|
||||
await saveProviderAccount(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
async setDefaultAccount(accountId: string): Promise<void> {
|
||||
await ensureProviderStoreMigrated();
|
||||
await setDefaultProviderAccount(accountId);
|
||||
}
|
||||
|
||||
getVendorDefinition(vendorId: string): ProviderDefinition | undefined {
|
||||
return getProviderDefinition(vendorId);
|
||||
}
|
||||
}
|
||||
|
||||
const providerService = new ProviderService();
|
||||
|
||||
export function getProviderService(): ProviderService {
|
||||
return providerService;
|
||||
}
|
||||
103
electron/services/providers/provider-store.ts
Normal file
103
electron/services/providers/provider-store.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { ProviderAccount, ProviderConfig, ProviderType } from '../../shared/providers/types';
|
||||
import { getProviderDefinition } from '../../shared/providers/registry';
|
||||
import { getClawXProviderStore } from './store-instance';
|
||||
|
||||
const PROVIDER_STORE_SCHEMA_VERSION = 1;
|
||||
|
||||
function inferAuthMode(type: ProviderType): ProviderAccount['authMode'] {
|
||||
if (type === 'ollama') {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
const definition = getProviderDefinition(type);
|
||||
if (definition?.defaultAuthMode) {
|
||||
return definition.defaultAuthMode;
|
||||
}
|
||||
|
||||
return 'api_key';
|
||||
}
|
||||
|
||||
export function providerConfigToAccount(
|
||||
config: ProviderConfig,
|
||||
options?: { isDefault?: boolean },
|
||||
): ProviderAccount {
|
||||
return {
|
||||
id: config.id,
|
||||
vendorId: config.type,
|
||||
label: config.name,
|
||||
authMode: inferAuthMode(config.type),
|
||||
baseUrl: config.baseUrl,
|
||||
apiProtocol: config.type === 'custom' || config.type === 'ollama'
|
||||
? 'openai-completions'
|
||||
: getProviderDefinition(config.type)?.providerConfig?.api,
|
||||
model: config.model,
|
||||
fallbackModels: config.fallbackModels,
|
||||
fallbackAccountIds: config.fallbackProviderIds,
|
||||
enabled: config.enabled,
|
||||
isDefault: options?.isDefault ?? false,
|
||||
createdAt: config.createdAt,
|
||||
updatedAt: config.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function providerAccountToConfig(account: ProviderAccount): ProviderConfig {
|
||||
return {
|
||||
id: account.id,
|
||||
name: account.label,
|
||||
type: account.vendorId,
|
||||
baseUrl: account.baseUrl,
|
||||
model: account.model,
|
||||
fallbackModels: account.fallbackModels,
|
||||
fallbackProviderIds: account.fallbackAccountIds,
|
||||
enabled: account.enabled,
|
||||
createdAt: account.createdAt,
|
||||
updatedAt: account.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listProviderAccounts(): Promise<ProviderAccount[]> {
|
||||
const store = await getClawXProviderStore();
|
||||
const accounts = store.get('providerAccounts') as Record<string, ProviderAccount> | undefined;
|
||||
return Object.values(accounts ?? {});
|
||||
}
|
||||
|
||||
export async function getProviderAccount(accountId: string): Promise<ProviderAccount | null> {
|
||||
const store = await getClawXProviderStore();
|
||||
const accounts = store.get('providerAccounts') as Record<string, ProviderAccount> | undefined;
|
||||
return accounts?.[accountId] ?? null;
|
||||
}
|
||||
|
||||
export async function saveProviderAccount(account: ProviderAccount): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const accounts = (store.get('providerAccounts') ?? {}) as Record<string, ProviderAccount>;
|
||||
accounts[account.id] = account;
|
||||
store.set('providerAccounts', accounts);
|
||||
store.set('schemaVersion', PROVIDER_STORE_SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
export async function deleteProviderAccount(accountId: string): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const accounts = (store.get('providerAccounts') ?? {}) as Record<string, ProviderAccount>;
|
||||
delete accounts[accountId];
|
||||
store.set('providerAccounts', accounts);
|
||||
|
||||
if (store.get('defaultProviderAccountId') === accountId) {
|
||||
store.delete('defaultProviderAccountId');
|
||||
}
|
||||
}
|
||||
|
||||
export async function setDefaultProviderAccount(accountId: string): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
store.set('defaultProviderAccountId', accountId);
|
||||
|
||||
const accounts = (store.get('providerAccounts') ?? {}) as Record<string, ProviderAccount>;
|
||||
for (const account of Object.values(accounts)) {
|
||||
account.isDefault = account.id === accountId;
|
||||
}
|
||||
store.set('providerAccounts', accounts);
|
||||
}
|
||||
|
||||
export async function getDefaultProviderAccountId(): Promise<string | undefined> {
|
||||
const store = await getClawXProviderStore();
|
||||
return store.get('defaultProviderAccountId') as string | undefined;
|
||||
}
|
||||
23
electron/services/providers/store-instance.ts
Normal file
23
electron/services/providers/store-instance.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Lazy-load electron-store (ESM module) from the main process only.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let providerStore: any = null;
|
||||
|
||||
export async function getClawXProviderStore() {
|
||||
if (!providerStore) {
|
||||
const Store = (await import('electron-store')).default;
|
||||
providerStore = new Store({
|
||||
name: 'clawx-providers',
|
||||
defaults: {
|
||||
schemaVersion: 0,
|
||||
providers: {} as Record<string, unknown>,
|
||||
providerAccounts: {} as Record<string, unknown>,
|
||||
apiKeys: {} as Record<string, string>,
|
||||
providerSecrets: {} as Record<string, unknown>,
|
||||
defaultProvider: null as string | null,
|
||||
defaultProviderAccountId: null as string | null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return providerStore;
|
||||
}
|
||||
82
electron/services/secrets/secret-store.ts
Normal file
82
electron/services/secrets/secret-store.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { ProviderSecret } from '../../shared/providers/types';
|
||||
import { getClawXProviderStore } from '../providers/store-instance';
|
||||
|
||||
export interface SecretStore {
|
||||
get(accountId: string): Promise<ProviderSecret | null>;
|
||||
set(secret: ProviderSecret): Promise<void>;
|
||||
delete(accountId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class ElectronStoreSecretStore implements SecretStore {
|
||||
async get(accountId: string): Promise<ProviderSecret | null> {
|
||||
const store = await getClawXProviderStore();
|
||||
const secrets = (store.get('providerSecrets') ?? {}) as Record<string, ProviderSecret>;
|
||||
const secret = secrets[accountId];
|
||||
if (secret) {
|
||||
return secret;
|
||||
}
|
||||
|
||||
const apiKeys = (store.get('apiKeys') ?? {}) as Record<string, string>;
|
||||
const apiKey = apiKeys[accountId];
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'api_key',
|
||||
accountId,
|
||||
apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
async set(secret: ProviderSecret): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const secrets = (store.get('providerSecrets') ?? {}) as Record<string, ProviderSecret>;
|
||||
secrets[secret.accountId] = secret;
|
||||
store.set('providerSecrets', secrets);
|
||||
|
||||
// Keep legacy apiKeys in sync until the rest of the app moves to account-based secrets.
|
||||
const apiKeys = (store.get('apiKeys') ?? {}) as Record<string, string>;
|
||||
if (secret.type === 'api_key') {
|
||||
apiKeys[secret.accountId] = secret.apiKey;
|
||||
} else if (secret.type === 'local') {
|
||||
if (secret.apiKey) {
|
||||
apiKeys[secret.accountId] = secret.apiKey;
|
||||
} else {
|
||||
delete apiKeys[secret.accountId];
|
||||
}
|
||||
} else {
|
||||
delete apiKeys[secret.accountId];
|
||||
}
|
||||
store.set('apiKeys', apiKeys);
|
||||
}
|
||||
|
||||
async delete(accountId: string): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const secrets = (store.get('providerSecrets') ?? {}) as Record<string, ProviderSecret>;
|
||||
delete secrets[accountId];
|
||||
store.set('providerSecrets', secrets);
|
||||
|
||||
const apiKeys = (store.get('apiKeys') ?? {}) as Record<string, string>;
|
||||
delete apiKeys[accountId];
|
||||
store.set('apiKeys', apiKeys);
|
||||
}
|
||||
}
|
||||
|
||||
const secretStore = new ElectronStoreSecretStore();
|
||||
|
||||
export function getSecretStore(): SecretStore {
|
||||
return secretStore;
|
||||
}
|
||||
|
||||
export async function getProviderSecret(accountId: string): Promise<ProviderSecret | null> {
|
||||
return getSecretStore().get(accountId);
|
||||
}
|
||||
|
||||
export async function setProviderSecret(secret: ProviderSecret): Promise<void> {
|
||||
await getSecretStore().set(secret);
|
||||
}
|
||||
|
||||
export async function deleteProviderSecret(accountId: string): Promise<void> {
|
||||
await getSecretStore().delete(accountId);
|
||||
}
|
||||
294
electron/shared/providers/registry.ts
Normal file
294
electron/shared/providers/registry.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import type {
|
||||
ProviderBackendConfig,
|
||||
ProviderDefinition,
|
||||
ProviderType,
|
||||
ProviderTypeInfo,
|
||||
} from './types';
|
||||
|
||||
export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
|
||||
{
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
icon: '🤖',
|
||||
placeholder: 'sk-ant-api03-...',
|
||||
model: 'Claude',
|
||||
requiresApiKey: true,
|
||||
category: 'official',
|
||||
envVar: 'ANTHROPIC_API_KEY',
|
||||
defaultModelId: 'claude-opus-4-6',
|
||||
supportedAuthModes: ['api_key'],
|
||||
defaultAuthMode: 'api_key',
|
||||
supportsMultipleAccounts: true,
|
||||
},
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
icon: '💚',
|
||||
placeholder: 'sk-proj-...',
|
||||
model: 'GPT',
|
||||
requiresApiKey: true,
|
||||
category: 'official',
|
||||
envVar: 'OPENAI_API_KEY',
|
||||
defaultModelId: 'gpt-5.2',
|
||||
supportedAuthModes: ['api_key'],
|
||||
defaultAuthMode: 'api_key',
|
||||
supportsMultipleAccounts: true,
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-responses',
|
||||
apiKeyEnv: 'OPENAI_API_KEY',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
icon: '🔷',
|
||||
placeholder: 'AIza...',
|
||||
model: 'Gemini',
|
||||
requiresApiKey: true,
|
||||
category: 'official',
|
||||
envVar: 'GEMINI_API_KEY',
|
||||
defaultModelId: 'gemini-3.1-pro-preview',
|
||||
isOAuth: true,
|
||||
supportsApiKey: true,
|
||||
supportedAuthModes: ['api_key', 'oauth_browser'],
|
||||
defaultAuthMode: 'api_key',
|
||||
supportsMultipleAccounts: true,
|
||||
},
|
||||
{
|
||||
id: 'openrouter',
|
||||
name: 'OpenRouter',
|
||||
icon: '🌐',
|
||||
placeholder: 'sk-or-v1-...',
|
||||
model: 'Multi-Model',
|
||||
requiresApiKey: true,
|
||||
showModelId: true,
|
||||
showModelIdInDevModeOnly: true,
|
||||
modelIdPlaceholder: 'anthropic/claude-opus-4.6',
|
||||
defaultModelId: 'anthropic/claude-opus-4.6',
|
||||
category: 'compatible',
|
||||
envVar: 'OPENROUTER_API_KEY',
|
||||
supportedAuthModes: ['api_key'],
|
||||
defaultAuthMode: 'api_key',
|
||||
supportsMultipleAccounts: true,
|
||||
providerConfig: {
|
||||
baseUrl: 'https://openrouter.ai/api/v1',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'OPENROUTER_API_KEY',
|
||||
headers: {
|
||||
'HTTP-Referer': 'https://claw-x.com',
|
||||
'X-Title': 'ClawX',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ark',
|
||||
name: 'ByteDance Ark',
|
||||
icon: 'A',
|
||||
placeholder: 'your-ark-api-key',
|
||||
model: 'Doubao',
|
||||
requiresApiKey: true,
|
||||
defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
showBaseUrl: true,
|
||||
showModelId: true,
|
||||
modelIdPlaceholder: 'ep-20260228000000-xxxxx',
|
||||
category: 'official',
|
||||
envVar: 'ARK_API_KEY',
|
||||
supportedAuthModes: ['api_key'],
|
||||
defaultAuthMode: 'api_key',
|
||||
supportsMultipleAccounts: true,
|
||||
providerConfig: {
|
||||
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'ARK_API_KEY',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'moonshot',
|
||||
name: 'Moonshot (CN)',
|
||||
icon: '🌙',
|
||||
placeholder: 'sk-...',
|
||||
model: 'Kimi',
|
||||
requiresApiKey: true,
|
||||
defaultBaseUrl: 'https://api.moonshot.cn/v1',
|
||||
defaultModelId: 'kimi-k2.5',
|
||||
category: 'official',
|
||||
envVar: 'MOONSHOT_API_KEY',
|
||||
supportedAuthModes: ['api_key'],
|
||||
defaultAuthMode: 'api_key',
|
||||
supportsMultipleAccounts: true,
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.moonshot.cn/v1',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'MOONSHOT_API_KEY',
|
||||
models: [
|
||||
{
|
||||
id: 'kimi-k2.5',
|
||||
name: 'Kimi K2.5',
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 256000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'siliconflow',
|
||||
name: 'SiliconFlow (CN)',
|
||||
icon: '🌊',
|
||||
placeholder: 'sk-...',
|
||||
model: 'Multi-Model',
|
||||
requiresApiKey: true,
|
||||
defaultBaseUrl: 'https://api.siliconflow.cn/v1',
|
||||
showModelId: true,
|
||||
showModelIdInDevModeOnly: true,
|
||||
modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3',
|
||||
defaultModelId: 'deepseek-ai/DeepSeek-V3',
|
||||
category: 'compatible',
|
||||
envVar: 'SILICONFLOW_API_KEY',
|
||||
supportedAuthModes: ['api_key'],
|
||||
defaultAuthMode: 'api_key',
|
||||
supportsMultipleAccounts: true,
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'SILICONFLOW_API_KEY',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'minimax-portal',
|
||||
name: 'MiniMax (Global)',
|
||||
icon: '☁️',
|
||||
placeholder: 'sk-...',
|
||||
model: 'MiniMax',
|
||||
requiresApiKey: false,
|
||||
isOAuth: true,
|
||||
supportsApiKey: true,
|
||||
defaultModelId: 'MiniMax-M2.5',
|
||||
apiKeyUrl: 'https://intl.minimaxi.com/',
|
||||
category: 'official',
|
||||
envVar: 'MINIMAX_API_KEY',
|
||||
supportedAuthModes: ['oauth_device', 'api_key'],
|
||||
defaultAuthMode: 'oauth_device',
|
||||
supportsMultipleAccounts: true,
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.minimax.io/anthropic',
|
||||
api: 'anthropic-messages',
|
||||
apiKeyEnv: 'MINIMAX_API_KEY',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'minimax-portal-cn',
|
||||
name: 'MiniMax (CN)',
|
||||
icon: '☁️',
|
||||
placeholder: 'sk-...',
|
||||
model: 'MiniMax',
|
||||
requiresApiKey: false,
|
||||
isOAuth: true,
|
||||
supportsApiKey: true,
|
||||
defaultModelId: 'MiniMax-M2.5',
|
||||
apiKeyUrl: 'https://platform.minimaxi.com/',
|
||||
category: 'official',
|
||||
envVar: 'MINIMAX_CN_API_KEY',
|
||||
supportedAuthModes: ['oauth_device', 'api_key'],
|
||||
defaultAuthMode: 'oauth_device',
|
||||
supportsMultipleAccounts: true,
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
api: 'anthropic-messages',
|
||||
apiKeyEnv: 'MINIMAX_CN_API_KEY',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'qwen-portal',
|
||||
name: 'Qwen',
|
||||
icon: '☁️',
|
||||
placeholder: 'sk-...',
|
||||
model: 'Qwen',
|
||||
requiresApiKey: false,
|
||||
isOAuth: true,
|
||||
defaultModelId: 'coder-model',
|
||||
category: 'official',
|
||||
envVar: 'QWEN_API_KEY',
|
||||
supportedAuthModes: ['oauth_device'],
|
||||
defaultAuthMode: 'oauth_device',
|
||||
supportsMultipleAccounts: true,
|
||||
providerConfig: {
|
||||
baseUrl: 'https://portal.qwen.ai/v1',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'QWEN_API_KEY',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ollama',
|
||||
name: 'Ollama',
|
||||
icon: '🦙',
|
||||
placeholder: 'Not required',
|
||||
requiresApiKey: false,
|
||||
defaultBaseUrl: 'http://localhost:11434/v1',
|
||||
showBaseUrl: true,
|
||||
showModelId: true,
|
||||
modelIdPlaceholder: 'qwen3:latest',
|
||||
category: 'local',
|
||||
supportedAuthModes: ['local'],
|
||||
defaultAuthMode: 'local',
|
||||
supportsMultipleAccounts: true,
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
name: 'Custom',
|
||||
icon: '⚙️',
|
||||
placeholder: 'API key...',
|
||||
requiresApiKey: true,
|
||||
showBaseUrl: true,
|
||||
showModelId: true,
|
||||
modelIdPlaceholder: 'your-provider/model-id',
|
||||
category: 'custom',
|
||||
envVar: 'CUSTOM_API_KEY',
|
||||
supportedAuthModes: ['api_key'],
|
||||
defaultAuthMode: 'api_key',
|
||||
supportsMultipleAccounts: true,
|
||||
},
|
||||
];
|
||||
|
||||
const PROVIDER_DEFINITION_MAP = new Map(
|
||||
PROVIDER_DEFINITIONS.map((definition) => [definition.id, definition]),
|
||||
);
|
||||
|
||||
export function getProviderDefinition(
|
||||
type: ProviderType | string,
|
||||
): ProviderDefinition | undefined {
|
||||
return PROVIDER_DEFINITION_MAP.get(type as ProviderType);
|
||||
}
|
||||
|
||||
export function getProviderTypeInfo(
|
||||
type: ProviderType,
|
||||
): ProviderTypeInfo | undefined {
|
||||
return getProviderDefinition(type);
|
||||
}
|
||||
|
||||
export function getProviderEnvVar(type: string): string | undefined {
|
||||
return getProviderDefinition(type)?.envVar;
|
||||
}
|
||||
|
||||
export function getProviderDefaultModel(type: string): string | undefined {
|
||||
return getProviderDefinition(type)?.defaultModelId;
|
||||
}
|
||||
|
||||
export function getProviderBackendConfig(
|
||||
type: string,
|
||||
): ProviderBackendConfig | undefined {
|
||||
return getProviderDefinition(type)?.providerConfig;
|
||||
}
|
||||
|
||||
export function getProviderUiInfoList(): ProviderTypeInfo[] {
|
||||
return PROVIDER_DEFINITIONS;
|
||||
}
|
||||
|
||||
export function getKeyableProviderTypes(): string[] {
|
||||
return PROVIDER_DEFINITIONS.filter((definition) => definition.envVar).map(
|
||||
(definition) => definition.id,
|
||||
);
|
||||
}
|
||||
169
electron/shared/providers/types.ts
Normal file
169
electron/shared/providers/types.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
export const PROVIDER_TYPES = [
|
||||
'anthropic',
|
||||
'openai',
|
||||
'google',
|
||||
'openrouter',
|
||||
'ark',
|
||||
'moonshot',
|
||||
'siliconflow',
|
||||
'minimax-portal',
|
||||
'minimax-portal-cn',
|
||||
'qwen-portal',
|
||||
'ollama',
|
||||
'custom',
|
||||
] as const;
|
||||
|
||||
export const BUILTIN_PROVIDER_TYPES = [
|
||||
'anthropic',
|
||||
'openai',
|
||||
'google',
|
||||
'openrouter',
|
||||
'ark',
|
||||
'moonshot',
|
||||
'siliconflow',
|
||||
'minimax-portal',
|
||||
'minimax-portal-cn',
|
||||
'qwen-portal',
|
||||
'ollama',
|
||||
] as const;
|
||||
|
||||
export type ProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
export type BuiltinProviderType = (typeof BUILTIN_PROVIDER_TYPES)[number];
|
||||
|
||||
export const OLLAMA_PLACEHOLDER_API_KEY = 'ollama-local';
|
||||
|
||||
export type ProviderProtocol =
|
||||
| 'openai-completions'
|
||||
| 'openai-responses'
|
||||
| 'anthropic-messages';
|
||||
|
||||
export type ProviderAuthMode =
|
||||
| 'api_key'
|
||||
| 'oauth_device'
|
||||
| 'oauth_browser'
|
||||
| 'local';
|
||||
|
||||
export type ProviderVendorCategory =
|
||||
| 'official'
|
||||
| 'compatible'
|
||||
| 'local'
|
||||
| 'custom';
|
||||
|
||||
export interface ProviderConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ProviderType;
|
||||
baseUrl?: string;
|
||||
model?: string;
|
||||
fallbackModels?: string[];
|
||||
fallbackProviderIds?: string[];
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProviderWithKeyInfo extends ProviderConfig {
|
||||
hasKey: boolean;
|
||||
keyMasked: string | null;
|
||||
}
|
||||
|
||||
export interface ProviderTypeInfo {
|
||||
id: ProviderType;
|
||||
name: string;
|
||||
icon: string;
|
||||
placeholder: string;
|
||||
model?: string;
|
||||
requiresApiKey: boolean;
|
||||
defaultBaseUrl?: string;
|
||||
showBaseUrl?: boolean;
|
||||
showModelId?: boolean;
|
||||
showModelIdInDevModeOnly?: boolean;
|
||||
modelIdPlaceholder?: string;
|
||||
defaultModelId?: string;
|
||||
isOAuth?: boolean;
|
||||
supportsApiKey?: boolean;
|
||||
apiKeyUrl?: string;
|
||||
}
|
||||
|
||||
export interface ProviderModelEntry extends Record<string, unknown> {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ProviderBackendConfig {
|
||||
baseUrl: string;
|
||||
api: ProviderProtocol;
|
||||
apiKeyEnv: string;
|
||||
models?: ProviderModelEntry[];
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ProviderDefinition extends ProviderTypeInfo {
|
||||
category: ProviderVendorCategory;
|
||||
envVar?: string;
|
||||
providerConfig?: ProviderBackendConfig;
|
||||
supportedAuthModes: ProviderAuthMode[];
|
||||
defaultAuthMode: ProviderAuthMode;
|
||||
supportsMultipleAccounts: boolean;
|
||||
}
|
||||
|
||||
export interface ProviderAccount {
|
||||
id: string;
|
||||
vendorId: ProviderType;
|
||||
label: string;
|
||||
authMode: ProviderAuthMode;
|
||||
baseUrl?: string;
|
||||
apiProtocol?: ProviderProtocol;
|
||||
model?: string;
|
||||
fallbackModels?: string[];
|
||||
fallbackAccountIds?: string[];
|
||||
enabled: boolean;
|
||||
isDefault: boolean;
|
||||
metadata?: {
|
||||
region?: string;
|
||||
email?: string;
|
||||
resourceUrl?: string;
|
||||
customModels?: string[];
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type ProviderSecret =
|
||||
| {
|
||||
type: 'api_key';
|
||||
accountId: string;
|
||||
apiKey: string;
|
||||
}
|
||||
| {
|
||||
type: 'oauth';
|
||||
accountId: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: number;
|
||||
scopes?: string[];
|
||||
email?: string;
|
||||
subject?: string;
|
||||
}
|
||||
| {
|
||||
type: 'local';
|
||||
accountId: string;
|
||||
apiKey?: string;
|
||||
};
|
||||
|
||||
export interface ModelSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
vendorId: string;
|
||||
accountId?: string;
|
||||
supportsVision?: boolean;
|
||||
supportsReasoning?: boolean;
|
||||
contextWindow?: number;
|
||||
pricing?: {
|
||||
input?: number;
|
||||
output?: number;
|
||||
cacheRead?: number;
|
||||
cacheWrite?: number;
|
||||
};
|
||||
source: 'builtin' | 'remote' | 'gateway' | 'custom';
|
||||
}
|
||||
@@ -1,147 +1,25 @@
|
||||
/**
|
||||
* Provider Registry — single source of truth for backend provider metadata.
|
||||
* Centralizes env var mappings, default models, and OpenClaw provider configs.
|
||||
*
|
||||
* NOTE: When adding a new provider type, also update src/lib/providers.ts
|
||||
* Backend compatibility layer around the shared provider registry.
|
||||
*/
|
||||
|
||||
export const BUILTIN_PROVIDER_TYPES = [
|
||||
'anthropic',
|
||||
'openai',
|
||||
'google',
|
||||
'openrouter',
|
||||
'ark',
|
||||
'moonshot',
|
||||
'siliconflow',
|
||||
'minimax-portal',
|
||||
'minimax-portal-cn',
|
||||
'qwen-portal',
|
||||
'ollama',
|
||||
] as const;
|
||||
export type BuiltinProviderType = (typeof BUILTIN_PROVIDER_TYPES)[number];
|
||||
export type ProviderType = BuiltinProviderType | 'custom';
|
||||
export {
|
||||
BUILTIN_PROVIDER_TYPES,
|
||||
type BuiltinProviderType,
|
||||
type ProviderType,
|
||||
} from '../shared/providers/types';
|
||||
import {
|
||||
type ProviderBackendConfig,
|
||||
type ProviderModelEntry,
|
||||
} from '../shared/providers/types';
|
||||
import {
|
||||
getKeyableProviderTypes as getSharedKeyableProviderTypes,
|
||||
getProviderBackendConfig,
|
||||
getProviderDefaultModel as getSharedProviderDefaultModel,
|
||||
getProviderEnvVar as getSharedProviderEnvVar,
|
||||
} from '../shared/providers/registry';
|
||||
|
||||
interface ProviderModelEntry extends Record<string, unknown> {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
|
||||
interface ProviderBackendMeta {
|
||||
envVar?: string;
|
||||
defaultModel?: string;
|
||||
/** OpenClaw models.providers config (omit for built-in providers like anthropic) */
|
||||
providerConfig?: {
|
||||
baseUrl: string;
|
||||
api: string;
|
||||
apiKeyEnv: string;
|
||||
models?: ProviderModelEntry[];
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
const REGISTRY: Record<string, ProviderBackendMeta> = {
|
||||
anthropic: {
|
||||
envVar: 'ANTHROPIC_API_KEY',
|
||||
defaultModel: 'anthropic/claude-opus-4-6',
|
||||
// anthropic is built-in to OpenClaw's model registry, no provider config needed
|
||||
},
|
||||
openai: {
|
||||
envVar: 'OPENAI_API_KEY',
|
||||
defaultModel: 'openai/gpt-5.2',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
api: 'openai-responses',
|
||||
apiKeyEnv: 'OPENAI_API_KEY',
|
||||
},
|
||||
},
|
||||
google: {
|
||||
envVar: 'GEMINI_API_KEY',
|
||||
defaultModel: 'google/gemini-3.1-pro-preview',
|
||||
// google is built-in to OpenClaw's pi-ai catalog, no providerConfig needed.
|
||||
// Adding models.providers.google overrides the built-in and can break Gemini.
|
||||
},
|
||||
openrouter: {
|
||||
envVar: 'OPENROUTER_API_KEY',
|
||||
defaultModel: 'openrouter/anthropic/claude-opus-4.6',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://openrouter.ai/api/v1',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'OPENROUTER_API_KEY',
|
||||
headers: {
|
||||
'HTTP-Referer': 'https://claw-x.com',
|
||||
'X-Title': 'ClawX',
|
||||
},
|
||||
},
|
||||
},
|
||||
ark: {
|
||||
envVar: 'ARK_API_KEY',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'ARK_API_KEY',
|
||||
},
|
||||
},
|
||||
moonshot: {
|
||||
envVar: 'MOONSHOT_API_KEY',
|
||||
defaultModel: 'moonshot/kimi-k2.5',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.moonshot.cn/v1',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'MOONSHOT_API_KEY',
|
||||
models: [
|
||||
{
|
||||
id: 'kimi-k2.5',
|
||||
name: 'Kimi K2.5',
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 256000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
siliconflow: {
|
||||
envVar: 'SILICONFLOW_API_KEY',
|
||||
defaultModel: 'siliconflow/deepseek-ai/DeepSeek-V3',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'SILICONFLOW_API_KEY',
|
||||
},
|
||||
},
|
||||
'minimax-portal': {
|
||||
envVar: 'MINIMAX_API_KEY',
|
||||
defaultModel: 'minimax-portal/MiniMax-M2.5',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.minimax.io/anthropic',
|
||||
api: 'anthropic-messages',
|
||||
apiKeyEnv: 'MINIMAX_API_KEY',
|
||||
},
|
||||
},
|
||||
'minimax-portal-cn': {
|
||||
envVar: 'MINIMAX_CN_API_KEY',
|
||||
defaultModel: 'minimax-portal/MiniMax-M2.5',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
api: 'anthropic-messages',
|
||||
apiKeyEnv: 'MINIMAX_CN_API_KEY',
|
||||
},
|
||||
},
|
||||
'qwen-portal': {
|
||||
envVar: 'QWEN_API_KEY',
|
||||
defaultModel: 'qwen-portal/coder-model',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://portal.qwen.ai/v1',
|
||||
api: 'openai-completions',
|
||||
apiKeyEnv: 'QWEN_API_KEY',
|
||||
},
|
||||
},
|
||||
custom: {
|
||||
envVar: 'CUSTOM_API_KEY',
|
||||
},
|
||||
// Additional providers with env var mappings but no default model
|
||||
// Additional env-backed providers that are not yet exposed in the UI.
|
||||
const EXTRA_ENV_ONLY_PROVIDERS: Record<string, { envVar: string }> = {
|
||||
groq: { envVar: 'GROQ_API_KEY' },
|
||||
deepgram: { envVar: 'DEEPGRAM_API_KEY' },
|
||||
cerebras: { envVar: 'CEREBRAS_API_KEY' },
|
||||
@@ -151,19 +29,19 @@ const REGISTRY: Record<string, ProviderBackendMeta> = {
|
||||
|
||||
/** Get the environment variable name for a provider type */
|
||||
export function getProviderEnvVar(type: string): string | undefined {
|
||||
return REGISTRY[type]?.envVar;
|
||||
return getSharedProviderEnvVar(type) ?? EXTRA_ENV_ONLY_PROVIDERS[type]?.envVar;
|
||||
}
|
||||
|
||||
/** Get the default model string for a provider type */
|
||||
export function getProviderDefaultModel(type: string): string | undefined {
|
||||
return REGISTRY[type]?.defaultModel;
|
||||
return getSharedProviderDefaultModel(type);
|
||||
}
|
||||
|
||||
/** Get the OpenClaw provider config (baseUrl, api, apiKeyEnv, models, headers) */
|
||||
export function getProviderConfig(
|
||||
type: string
|
||||
): { baseUrl: string; api: string; apiKeyEnv: string; models?: ProviderModelEntry[]; headers?: Record<string, string> } | undefined {
|
||||
return REGISTRY[type]?.providerConfig;
|
||||
return getProviderBackendConfig(type) as ProviderBackendConfig | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +49,5 @@ export function getProviderConfig(
|
||||
* Used by GatewayManager to inject API keys as env vars.
|
||||
*/
|
||||
export function getKeyableProviderTypes(): string[] {
|
||||
return Object.entries(REGISTRY)
|
||||
.filter(([, meta]) => meta.envVar)
|
||||
.map(([type]) => type);
|
||||
return [...getSharedKeyableProviderTypes(), ...Object.keys(EXTRA_ENV_ONLY_PROVIDERS)];
|
||||
}
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
/**
|
||||
* Provider Storage
|
||||
* Manages provider configurations and API keys.
|
||||
* Keys are stored in plain text alongside provider configs in a single electron-store.
|
||||
* This file remains the legacy compatibility layer while the app migrates to
|
||||
* account-based provider storage and a dedicated secret-store abstraction.
|
||||
*/
|
||||
|
||||
import { BUILTIN_PROVIDER_TYPES, type ProviderType } from './provider-registry';
|
||||
import { getActiveOpenClawProviders } from './openclaw-auth';
|
||||
|
||||
// Lazy-load electron-store (ESM module)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let providerStore: any = null;
|
||||
|
||||
async function getProviderStore() {
|
||||
if (!providerStore) {
|
||||
const Store = (await import('electron-store')).default;
|
||||
providerStore = new Store({
|
||||
name: 'clawx-providers',
|
||||
defaults: {
|
||||
providers: {} as Record<string, ProviderConfig>,
|
||||
apiKeys: {} as Record<string, string>,
|
||||
defaultProvider: null as string | null,
|
||||
},
|
||||
});
|
||||
}
|
||||
return providerStore;
|
||||
}
|
||||
import {
|
||||
deleteProviderAccount,
|
||||
getProviderAccount,
|
||||
listProviderAccounts,
|
||||
providerAccountToConfig,
|
||||
providerConfigToAccount,
|
||||
saveProviderAccount,
|
||||
setDefaultProviderAccount,
|
||||
} from '../services/providers/provider-store';
|
||||
import { ensureProviderStoreMigrated } from '../services/providers/provider-migration';
|
||||
import { getClawXProviderStore } from '../services/providers/store-instance';
|
||||
import {
|
||||
deleteProviderSecret,
|
||||
getProviderSecret,
|
||||
setProviderSecret,
|
||||
} from '../services/secrets/secret-store';
|
||||
|
||||
/**
|
||||
* Provider configuration
|
||||
@@ -49,10 +47,16 @@ export interface ProviderConfig {
|
||||
*/
|
||||
export async function storeApiKey(providerId: string, apiKey: string): Promise<boolean> {
|
||||
try {
|
||||
const s = await getProviderStore();
|
||||
await ensureProviderStoreMigrated();
|
||||
const s = await getClawXProviderStore();
|
||||
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
|
||||
keys[providerId] = apiKey;
|
||||
s.set('apiKeys', keys);
|
||||
await setProviderSecret({
|
||||
type: 'api_key',
|
||||
accountId: providerId,
|
||||
apiKey,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to store API key:', error);
|
||||
@@ -65,7 +69,16 @@ export async function storeApiKey(providerId: string, apiKey: string): Promise<b
|
||||
*/
|
||||
export async function getApiKey(providerId: string): Promise<string | null> {
|
||||
try {
|
||||
const s = await getProviderStore();
|
||||
await ensureProviderStoreMigrated();
|
||||
const secret = await getProviderSecret(providerId);
|
||||
if (secret?.type === 'api_key') {
|
||||
return secret.apiKey;
|
||||
}
|
||||
if (secret?.type === 'local') {
|
||||
return secret.apiKey ?? null;
|
||||
}
|
||||
|
||||
const s = await getClawXProviderStore();
|
||||
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
|
||||
return keys[providerId] || null;
|
||||
} catch (error) {
|
||||
@@ -79,10 +92,12 @@ export async function getApiKey(providerId: string): Promise<string | null> {
|
||||
*/
|
||||
export async function deleteApiKey(providerId: string): Promise<boolean> {
|
||||
try {
|
||||
const s = await getProviderStore();
|
||||
await ensureProviderStoreMigrated();
|
||||
const s = await getClawXProviderStore();
|
||||
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
|
||||
delete keys[providerId];
|
||||
s.set('apiKeys', keys);
|
||||
await deleteProviderSecret(providerId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to delete API key:', error);
|
||||
@@ -94,7 +109,13 @@ export async function deleteApiKey(providerId: string): Promise<boolean> {
|
||||
* Check if an API key exists for a provider
|
||||
*/
|
||||
export async function hasApiKey(providerId: string): Promise<boolean> {
|
||||
const s = await getProviderStore();
|
||||
await ensureProviderStoreMigrated();
|
||||
const secret = await getProviderSecret(providerId);
|
||||
if (secret?.type === 'api_key') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const s = await getClawXProviderStore();
|
||||
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
|
||||
return providerId in keys;
|
||||
}
|
||||
@@ -103,7 +124,8 @@ export async function hasApiKey(providerId: string): Promise<boolean> {
|
||||
* List all provider IDs that have stored keys
|
||||
*/
|
||||
export async function listStoredKeyIds(): Promise<string[]> {
|
||||
const s = await getProviderStore();
|
||||
await ensureProviderStoreMigrated();
|
||||
const s = await getClawXProviderStore();
|
||||
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
|
||||
return Object.keys(keys);
|
||||
}
|
||||
@@ -114,28 +136,47 @@ export async function listStoredKeyIds(): Promise<string[]> {
|
||||
* Save a provider configuration
|
||||
*/
|
||||
export async function saveProvider(config: ProviderConfig): Promise<void> {
|
||||
const s = await getProviderStore();
|
||||
await ensureProviderStoreMigrated();
|
||||
const s = await getClawXProviderStore();
|
||||
const providers = s.get('providers') as Record<string, ProviderConfig>;
|
||||
providers[config.id] = config;
|
||||
s.set('providers', providers);
|
||||
|
||||
const defaultProviderId = (s.get('defaultProvider') ?? null) as string | null;
|
||||
await saveProviderAccount(
|
||||
providerConfigToAccount(config, { isDefault: defaultProviderId === config.id }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a provider configuration
|
||||
*/
|
||||
export async function getProvider(providerId: string): Promise<ProviderConfig | null> {
|
||||
const s = await getProviderStore();
|
||||
await ensureProviderStoreMigrated();
|
||||
const s = await getClawXProviderStore();
|
||||
const providers = s.get('providers') as Record<string, ProviderConfig>;
|
||||
return providers[providerId] || null;
|
||||
if (providers[providerId]) {
|
||||
return providers[providerId];
|
||||
}
|
||||
|
||||
const account = await getProviderAccount(providerId);
|
||||
return account ? providerAccountToConfig(account) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all provider configurations
|
||||
*/
|
||||
export async function getAllProviders(): Promise<ProviderConfig[]> {
|
||||
const s = await getProviderStore();
|
||||
await ensureProviderStoreMigrated();
|
||||
const s = await getClawXProviderStore();
|
||||
const providers = s.get('providers') as Record<string, ProviderConfig>;
|
||||
return Object.values(providers);
|
||||
const legacyProviders = Object.values(providers);
|
||||
if (legacyProviders.length > 0) {
|
||||
return legacyProviders;
|
||||
}
|
||||
|
||||
const accounts = await listProviderAccounts();
|
||||
return accounts.map(providerAccountToConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,18 +184,21 @@ export async function getAllProviders(): Promise<ProviderConfig[]> {
|
||||
*/
|
||||
export async function deleteProvider(providerId: string): Promise<boolean> {
|
||||
try {
|
||||
await ensureProviderStoreMigrated();
|
||||
// Delete the API key
|
||||
await deleteApiKey(providerId);
|
||||
|
||||
// Delete the provider config
|
||||
const s = await getProviderStore();
|
||||
const s = await getClawXProviderStore();
|
||||
const providers = s.get('providers') as Record<string, ProviderConfig>;
|
||||
delete providers[providerId];
|
||||
s.set('providers', providers);
|
||||
await deleteProviderAccount(providerId);
|
||||
|
||||
// Clear default if this was the default
|
||||
if (s.get('defaultProvider') === providerId) {
|
||||
s.delete('defaultProvider');
|
||||
s.delete('defaultProviderAccountId');
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -168,16 +212,20 @@ export async function deleteProvider(providerId: string): Promise<boolean> {
|
||||
* Set the default provider
|
||||
*/
|
||||
export async function setDefaultProvider(providerId: string): Promise<void> {
|
||||
const s = await getProviderStore();
|
||||
await ensureProviderStoreMigrated();
|
||||
const s = await getClawXProviderStore();
|
||||
s.set('defaultProvider', providerId);
|
||||
await setDefaultProviderAccount(providerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default provider
|
||||
*/
|
||||
export async function getDefaultProvider(): Promise<string | undefined> {
|
||||
const s = await getProviderStore();
|
||||
return s.get('defaultProvider') as string | undefined;
|
||||
await ensureProviderStoreMigrated();
|
||||
const s = await getClawXProviderStore();
|
||||
return (s.get('defaultProvider') as string | undefined)
|
||||
?? (s.get('defaultProviderAccountId') as string | undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Provider Types & UI Metadata — single source of truth for the frontend.
|
||||
*
|
||||
* NOTE: When adding a new provider type, also update
|
||||
* electron/utils/provider-registry.ts (env vars, models, configs).
|
||||
* NOTE: Backend provider metadata is being refactored toward the new
|
||||
* account-based registry, but the renderer still keeps a local compatibility
|
||||
* layer so TypeScript project boundaries remain stable during the migration.
|
||||
*/
|
||||
|
||||
export const PROVIDER_TYPES = [
|
||||
@@ -21,6 +22,20 @@ export const PROVIDER_TYPES = [
|
||||
] as const;
|
||||
export type ProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
|
||||
export const BUILTIN_PROVIDER_TYPES = [
|
||||
'anthropic',
|
||||
'openai',
|
||||
'google',
|
||||
'openrouter',
|
||||
'ark',
|
||||
'moonshot',
|
||||
'siliconflow',
|
||||
'minimax-portal',
|
||||
'minimax-portal-cn',
|
||||
'qwen-portal',
|
||||
'ollama',
|
||||
] as const;
|
||||
|
||||
export const OLLAMA_PLACEHOLDER_API_KEY = 'ollama-local';
|
||||
|
||||
export interface ProviderConfig {
|
||||
@@ -46,26 +61,16 @@ export interface ProviderTypeInfo {
|
||||
name: string;
|
||||
icon: string;
|
||||
placeholder: string;
|
||||
/** Model brand name for display (e.g. "Claude", "GPT") */
|
||||
model?: string;
|
||||
requiresApiKey: boolean;
|
||||
/** Pre-filled base URL (for proxy/compatible providers like SiliconFlow) */
|
||||
defaultBaseUrl?: string;
|
||||
/** Whether the user can edit the base URL in setup */
|
||||
showBaseUrl?: boolean;
|
||||
/** Whether to show a Model ID input field (for providers where user picks the model) */
|
||||
showModelId?: boolean;
|
||||
/** Whether the Model ID input should only be shown in developer mode */
|
||||
showModelIdInDevModeOnly?: boolean;
|
||||
/** Default / example model ID placeholder */
|
||||
modelIdPlaceholder?: string;
|
||||
/** Default model ID to pre-fill */
|
||||
defaultModelId?: string;
|
||||
/** Whether this provider uses OAuth device flow instead of an API key */
|
||||
isOAuth?: boolean;
|
||||
/** Whether this provider also accepts a direct API key (in addition to OAuth) */
|
||||
supportsApiKey?: boolean;
|
||||
/** URL where users can apply for the API Key */
|
||||
apiKeyUrl?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user