314 lines
9.7 KiB
TypeScript
314 lines
9.7 KiB
TypeScript
/**
|
|
* Provider Storage
|
|
* Manages provider configurations and API keys.
|
|
* 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';
|
|
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';
|
|
import { getOpenClawProviderKeyForType } from './provider-keys';
|
|
|
|
/**
|
|
* Provider configuration
|
|
*/
|
|
export interface ProviderConfig {
|
|
id: string;
|
|
name: string;
|
|
type: ProviderType;
|
|
baseUrl?: string;
|
|
apiProtocol?: 'openai-completions' | 'openai-responses' | 'anthropic-messages';
|
|
headers?: Record<string, string>;
|
|
model?: string;
|
|
fallbackModels?: string[];
|
|
fallbackProviderIds?: string[];
|
|
enabled: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
// ==================== API Key Storage ====================
|
|
|
|
/**
|
|
* Store an API key
|
|
*/
|
|
export async function storeApiKey(providerId: string, apiKey: string): Promise<boolean> {
|
|
try {
|
|
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);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve an API key
|
|
*/
|
|
export async function getApiKey(providerId: string): Promise<string | null> {
|
|
try {
|
|
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) {
|
|
console.error('Failed to retrieve API key:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an API key
|
|
*/
|
|
export async function deleteApiKey(providerId: string): Promise<boolean> {
|
|
try {
|
|
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);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an API key exists for a provider
|
|
*/
|
|
export async function hasApiKey(providerId: string): Promise<boolean> {
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* List all provider IDs that have stored keys
|
|
*/
|
|
export async function listStoredKeyIds(): Promise<string[]> {
|
|
await ensureProviderStoreMigrated();
|
|
const s = await getClawXProviderStore();
|
|
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
|
|
return Object.keys(keys);
|
|
}
|
|
|
|
// ==================== Provider Configuration ====================
|
|
|
|
/**
|
|
* Save a provider configuration
|
|
*/
|
|
export async function saveProvider(config: ProviderConfig): Promise<void> {
|
|
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> {
|
|
await ensureProviderStoreMigrated();
|
|
const s = await getClawXProviderStore();
|
|
const providers = s.get('providers') as Record<string, ProviderConfig>;
|
|
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[]> {
|
|
await ensureProviderStoreMigrated();
|
|
const s = await getClawXProviderStore();
|
|
const providers = s.get('providers') as Record<string, ProviderConfig>;
|
|
const legacyProviders = Object.values(providers);
|
|
if (legacyProviders.length > 0) {
|
|
return legacyProviders;
|
|
}
|
|
|
|
const accounts = await listProviderAccounts();
|
|
return accounts.map(providerAccountToConfig);
|
|
}
|
|
|
|
/**
|
|
* Delete a provider configuration and its API key
|
|
*/
|
|
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 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;
|
|
} catch (error) {
|
|
console.error('Failed to delete provider:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the default provider
|
|
*/
|
|
export async function setDefaultProvider(providerId: string): Promise<void> {
|
|
await ensureProviderStoreMigrated();
|
|
const s = await getClawXProviderStore();
|
|
s.set('defaultProvider', providerId);
|
|
await setDefaultProviderAccount(providerId);
|
|
}
|
|
|
|
/**
|
|
* Get the default provider
|
|
*/
|
|
export async function getDefaultProvider(): Promise<string | undefined> {
|
|
await ensureProviderStoreMigrated();
|
|
const s = await getClawXProviderStore();
|
|
return (s.get('defaultProvider') as string | undefined)
|
|
?? (s.get('defaultProviderAccountId') as string | undefined);
|
|
}
|
|
|
|
/**
|
|
* Get provider with masked key info (for UI display)
|
|
*/
|
|
export async function getProviderWithKeyInfo(
|
|
providerId: string
|
|
): Promise<(ProviderConfig & { hasKey: boolean; keyMasked: string | null }) | null> {
|
|
const provider = await getProvider(providerId);
|
|
if (!provider) return null;
|
|
|
|
const apiKey = await getApiKey(providerId);
|
|
let keyMasked: string | null = null;
|
|
|
|
if (apiKey) {
|
|
if (apiKey.length > 12) {
|
|
keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`;
|
|
} else {
|
|
keyMasked = '*'.repeat(apiKey.length);
|
|
}
|
|
}
|
|
|
|
return {
|
|
...provider,
|
|
hasKey: !!apiKey,
|
|
keyMasked,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get all providers with key info (for UI display)
|
|
* Also synchronizes ClawX local provider list with OpenClaw's actual config.
|
|
*/
|
|
export async function getAllProvidersWithKeyInfo(): Promise<
|
|
Array<ProviderConfig & { hasKey: boolean; keyMasked: string | null }>
|
|
> {
|
|
const providers = await getAllProviders();
|
|
const results: Array<ProviderConfig & { hasKey: boolean; keyMasked: string | null }> = [];
|
|
const activeOpenClawProviders = await getActiveOpenClawProviders();
|
|
// When openclaw.json is deleted/missing, the active set is empty.
|
|
// In that case, all providers (including builtins) should be cleaned up.
|
|
const configMissing = activeOpenClawProviders.size === 0;
|
|
|
|
for (const provider of providers) {
|
|
// Sync check: If it's a custom/OAuth provider and it no longer exists in OpenClaw config
|
|
// (e.g. wiped by Gateway due to missing plugin, or manually deleted by user)
|
|
// we should remove it from ClawX UI to stay consistent.
|
|
const isBuiltin = (BUILTIN_PROVIDER_TYPES as readonly string[]).includes(provider.type);
|
|
// For custom/ollama providers, the OpenClaw config key is derived as
|
|
// "<type>-<suffix>" where suffix = first 8 chars of providerId with hyphens stripped.
|
|
// e.g. provider.id "custom-a1b2c3d4-..." → strip hyphens → "customa1b2c3d4..." → slice(0,8) → "customa1"
|
|
// → openClawKey = "custom-customa1"
|
|
// This must match getOpenClawProviderKey() in ipc-handlers.ts exactly.
|
|
const openClawKey = getOpenClawProviderKeyForType(provider.type, provider.id);
|
|
const isActive = activeOpenClawProviders.has(provider.type) || activeOpenClawProviders.has(provider.id) || activeOpenClawProviders.has(openClawKey);
|
|
if (configMissing || (!isBuiltin && !isActive)) {
|
|
console.log(`[Sync] Provider ${provider.id} (${provider.type}) missing from OpenClaw, hiding from UI`);
|
|
// Skip from display but don't delete from store — preserves API key
|
|
// associations so that restoring openclaw.json brings accounts back intact.
|
|
continue;
|
|
}
|
|
|
|
const apiKey = await getApiKey(provider.id);
|
|
let keyMasked: string | null = null;
|
|
|
|
if (apiKey) {
|
|
if (apiKey.length > 12) {
|
|
keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`;
|
|
} else {
|
|
keyMasked = '*'.repeat(apiKey.length);
|
|
}
|
|
}
|
|
|
|
results.push({
|
|
...provider,
|
|
hasKey: !!apiKey,
|
|
keyMasked,
|
|
});
|
|
}
|
|
|
|
return results;
|
|
}
|