Files
DeskClaw/electron/services/providers/provider-runtime-sync.ts
Lingxuan Zuo e28eba01e1 refactor/channel & ipc (#349)
Co-authored-by: paisley <8197966+su8su@users.noreply.github.com>
Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
2026-03-09 19:04:00 +08:00

482 lines
15 KiB
TypeScript

import type { GatewayManager } from '../../gateway/manager';
import { getProviderAccount, listProviderAccounts } from './provider-store';
import { getProviderSecret } from '../secrets/secret-store';
import type { ProviderConfig } from '../../utils/secure-storage';
import { getAllProviders, getApiKey, getDefaultProvider, getProvider } from '../../utils/secure-storage';
import { getProviderConfig, getProviderDefaultModel } from '../../utils/provider-registry';
import {
removeProviderFromOpenClaw,
saveOAuthTokenToOpenClaw,
saveProviderKeyToOpenClaw,
setOpenClawDefaultModel,
setOpenClawDefaultModelWithOverride,
syncProviderConfigToOpenClaw,
updateAgentModelProvider,
} from '../../utils/openclaw-auth';
import { logger } from '../../utils/logger';
const GOOGLE_OAUTH_RUNTIME_PROVIDER = 'google-gemini-cli';
const GOOGLE_OAUTH_DEFAULT_MODEL_REF = `${GOOGLE_OAUTH_RUNTIME_PROVIDER}/gemini-3-pro-preview`;
type RuntimeProviderSyncContext = {
runtimeProviderKey: string;
meta: ReturnType<typeof getProviderConfig>;
api: string;
};
export function getOpenClawProviderKey(type: string, providerId: string): string {
if (type === 'custom' || type === 'ollama') {
const suffix = providerId.replace(/-/g, '').slice(0, 8);
return `${type}-${suffix}`;
}
if (type === 'minimax-portal-cn') {
return 'minimax-portal';
}
return type;
}
async function resolveRuntimeProviderKey(config: ProviderConfig): Promise<string> {
const account = await getProviderAccount(config.id);
if (config.type === 'google' && account?.authMode === 'oauth_browser') {
return GOOGLE_OAUTH_RUNTIME_PROVIDER;
}
return getOpenClawProviderKey(config.type, config.id);
}
async function isGoogleBrowserOAuthProvider(config: ProviderConfig): Promise<boolean> {
const account = await getProviderAccount(config.id);
if (config.type !== 'google' || account?.authMode !== 'oauth_browser') {
return false;
}
const secret = await getProviderSecret(config.id);
return secret?.type === 'oauth';
}
export function getProviderModelRef(config: ProviderConfig): string | undefined {
const providerKey = getOpenClawProviderKey(config.type, config.id);
if (config.model) {
return config.model.startsWith(`${providerKey}/`)
? config.model
: `${providerKey}/${config.model}`;
}
const defaultModel = getProviderDefaultModel(config.type);
if (!defaultModel) {
return undefined;
}
return defaultModel.startsWith(`${providerKey}/`)
? defaultModel
: `${providerKey}/${defaultModel}`;
}
export async function getProviderFallbackModelRefs(config: ProviderConfig): Promise<string[]> {
const allProviders = await getAllProviders();
const providerMap = new Map(allProviders.map((provider) => [provider.id, provider]));
const seen = new Set<string>();
const results: string[] = [];
const providerKey = getOpenClawProviderKey(config.type, config.id);
for (const fallbackModel of config.fallbackModels ?? []) {
const normalizedModel = fallbackModel.trim();
if (!normalizedModel) continue;
const modelRef = normalizedModel.startsWith(`${providerKey}/`)
? normalizedModel
: `${providerKey}/${normalizedModel}`;
if (seen.has(modelRef)) continue;
seen.add(modelRef);
results.push(modelRef);
}
for (const fallbackId of config.fallbackProviderIds ?? []) {
if (!fallbackId || fallbackId === config.id) continue;
const fallbackProvider = providerMap.get(fallbackId);
if (!fallbackProvider) continue;
const modelRef = getProviderModelRef(fallbackProvider);
if (!modelRef || seen.has(modelRef)) continue;
seen.add(modelRef);
results.push(modelRef);
}
return results;
}
function scheduleGatewayRestart(
gatewayManager: GatewayManager | undefined,
message: string,
options?: { delayMs?: number; onlyIfRunning?: boolean },
): void {
if (!gatewayManager) {
return;
}
if (options?.onlyIfRunning && gatewayManager.getStatus().state === 'stopped') {
return;
}
logger.info(message);
gatewayManager.debouncedRestart(options?.delayMs);
}
export async function syncProviderApiKeyToRuntime(
providerType: string,
providerId: string,
apiKey: string,
): Promise<void> {
const ock = getOpenClawProviderKey(providerType, providerId);
await saveProviderKeyToOpenClaw(ock, apiKey);
}
export async function syncAllProviderAuthToRuntime(): Promise<void> {
const accounts = await listProviderAccounts();
for (const account of accounts) {
const runtimeProviderKey = await resolveRuntimeProviderKey({
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,
});
const secret = await getProviderSecret(account.id);
if (!secret) {
continue;
}
if (secret.type === 'api_key') {
await saveProviderKeyToOpenClaw(runtimeProviderKey, secret.apiKey);
continue;
}
if (secret.type === 'local' && secret.apiKey) {
await saveProviderKeyToOpenClaw(runtimeProviderKey, secret.apiKey);
continue;
}
if (secret.type === 'oauth') {
await saveOAuthTokenToOpenClaw(runtimeProviderKey, {
access: secret.accessToken,
refresh: secret.refreshToken,
expires: secret.expiresAt,
email: secret.email,
projectId: secret.subject,
});
}
}
}
async function syncProviderSecretToRuntime(
config: ProviderConfig,
runtimeProviderKey: string,
apiKey: string | undefined,
): Promise<void> {
const secret = await getProviderSecret(config.id);
if (apiKey !== undefined) {
const trimmedKey = apiKey.trim();
if (trimmedKey) {
await saveProviderKeyToOpenClaw(runtimeProviderKey, trimmedKey);
}
return;
}
if (secret?.type === 'api_key') {
await saveProviderKeyToOpenClaw(runtimeProviderKey, secret.apiKey);
return;
}
if (secret?.type === 'oauth') {
await saveOAuthTokenToOpenClaw(runtimeProviderKey, {
access: secret.accessToken,
refresh: secret.refreshToken,
expires: secret.expiresAt,
email: secret.email,
projectId: secret.subject,
});
return;
}
if (secret?.type === 'local' && secret.apiKey) {
await saveProviderKeyToOpenClaw(runtimeProviderKey, secret.apiKey);
}
}
async function resolveRuntimeSyncContext(config: ProviderConfig): Promise<RuntimeProviderSyncContext | null> {
const runtimeProviderKey = await resolveRuntimeProviderKey(config);
const meta = getProviderConfig(config.type);
const api = config.type === 'custom' || config.type === 'ollama' ? 'openai-completions' : meta?.api;
if (!api) {
return null;
}
return {
runtimeProviderKey,
meta,
api,
};
}
async function syncRuntimeProviderConfig(
config: ProviderConfig,
context: RuntimeProviderSyncContext,
): Promise<void> {
await syncProviderConfigToOpenClaw(context.runtimeProviderKey, config.model, {
baseUrl: config.baseUrl || context.meta?.baseUrl,
api: context.api,
apiKeyEnv: context.meta?.apiKeyEnv,
headers: context.meta?.headers,
});
}
async function syncCustomProviderAgentModel(
config: ProviderConfig,
runtimeProviderKey: string,
apiKey: string | undefined,
): Promise<void> {
if (config.type !== 'custom' && config.type !== 'ollama') {
return;
}
const resolvedKey = apiKey !== undefined ? (apiKey.trim() || null) : await getApiKey(config.id);
if (!resolvedKey || !config.baseUrl) {
return;
}
const modelId = config.model;
await updateAgentModelProvider(runtimeProviderKey, {
baseUrl: config.baseUrl,
api: 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [],
apiKey: resolvedKey,
});
}
async function syncProviderToRuntime(
config: ProviderConfig,
apiKey: string | undefined,
): Promise<RuntimeProviderSyncContext | null> {
const context = await resolveRuntimeSyncContext(config);
if (!context) {
return null;
}
await syncProviderSecretToRuntime(config, context.runtimeProviderKey, apiKey);
await syncRuntimeProviderConfig(config, context);
await syncCustomProviderAgentModel(config, context.runtimeProviderKey, apiKey);
return context;
}
export async function syncSavedProviderToRuntime(
config: ProviderConfig,
apiKey: string | undefined,
gatewayManager?: GatewayManager,
): Promise<void> {
const context = await syncProviderToRuntime(config, apiKey);
if (!context) {
return;
}
scheduleGatewayRestart(
gatewayManager,
`Scheduling Gateway restart after saving provider "${context.runtimeProviderKey}" config`,
);
}
export async function syncUpdatedProviderToRuntime(
config: ProviderConfig,
apiKey: string | undefined,
gatewayManager?: GatewayManager,
): Promise<void> {
const context = await syncProviderToRuntime(config, apiKey);
if (!context) {
return;
}
const ock = context.runtimeProviderKey;
const fallbackModels = await getProviderFallbackModelRefs(config);
const defaultProviderId = await getDefaultProvider();
if (defaultProviderId === config.id) {
const modelOverride = config.model ? `${ock}/${config.model}` : undefined;
if (config.type !== 'custom' && config.type !== 'ollama') {
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
} else {
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
baseUrl: config.baseUrl,
api: 'openai-completions',
}, fallbackModels);
}
}
scheduleGatewayRestart(
gatewayManager,
`Scheduling Gateway restart after updating provider "${ock}" config`,
);
}
export async function syncDeletedProviderToRuntime(
provider: ProviderConfig | null,
providerId: string,
gatewayManager?: GatewayManager,
runtimeProviderKey?: string,
): Promise<void> {
if (!provider?.type) {
return;
}
const ock = runtimeProviderKey ?? await resolveRuntimeProviderKey({ ...provider, id: providerId });
await removeProviderFromOpenClaw(ock);
scheduleGatewayRestart(
gatewayManager,
`Scheduling Gateway restart after deleting provider "${ock}"`,
);
}
export async function syncDeletedProviderApiKeyToRuntime(
provider: ProviderConfig | null,
providerId: string,
runtimeProviderKey?: string,
): Promise<void> {
if (!provider?.type) {
return;
}
const ock = runtimeProviderKey ?? await resolveRuntimeProviderKey({ ...provider, id: providerId });
await removeProviderFromOpenClaw(ock);
}
export async function syncDefaultProviderToRuntime(
providerId: string,
gatewayManager?: GatewayManager,
): Promise<void> {
const provider = await getProvider(providerId);
if (!provider) {
return;
}
const ock = await resolveRuntimeProviderKey(provider);
const providerKey = await getApiKey(providerId);
const fallbackModels = await getProviderFallbackModelRefs(provider);
const oauthTypes = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
const isGoogleOAuthProvider = await isGoogleBrowserOAuthProvider(provider);
const isOAuthProvider = (oauthTypes.includes(provider.type) && !providerKey) || isGoogleOAuthProvider;
if (!isOAuthProvider) {
const modelOverride = provider.model
? (provider.model.startsWith(`${ock}/`) ? provider.model : `${ock}/${provider.model}`)
: undefined;
if (provider.type === 'custom' || provider.type === 'ollama') {
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
baseUrl: provider.baseUrl,
api: 'openai-completions',
}, fallbackModels);
} else {
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
}
if (providerKey) {
await saveProviderKeyToOpenClaw(ock, providerKey);
}
} else {
if (isGoogleOAuthProvider) {
const secret = await getProviderSecret(provider.id);
if (secret?.type === 'oauth') {
await saveOAuthTokenToOpenClaw(GOOGLE_OAUTH_RUNTIME_PROVIDER, {
access: secret.accessToken,
refresh: secret.refreshToken,
expires: secret.expiresAt,
email: secret.email,
projectId: secret.subject,
});
}
const modelOverride = provider.model
? (provider.model.startsWith(`${GOOGLE_OAUTH_RUNTIME_PROVIDER}/`)
? provider.model
: `${GOOGLE_OAUTH_RUNTIME_PROVIDER}/${provider.model}`)
: GOOGLE_OAUTH_DEFAULT_MODEL_REF;
await setOpenClawDefaultModel(GOOGLE_OAUTH_RUNTIME_PROVIDER, modelOverride, fallbackModels);
logger.info(`Configured openclaw.json for Google browser OAuth provider "${provider.id}"`);
scheduleGatewayRestart(
gatewayManager,
`Scheduling Gateway restart after provider switch to "${GOOGLE_OAUTH_RUNTIME_PROVIDER}"`,
);
return;
}
const defaultBaseUrl = provider.type === 'minimax-portal'
? 'https://api.minimax.io/anthropic'
: (provider.type === 'minimax-portal-cn' ? 'https://api.minimaxi.com/anthropic' : 'https://portal.qwen.ai/v1');
const api: 'anthropic-messages' | 'openai-completions' =
(provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn')
? 'anthropic-messages'
: 'openai-completions';
let baseUrl = provider.baseUrl || defaultBaseUrl;
if ((provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn') && baseUrl) {
baseUrl = baseUrl.replace(/\/v1$/, '').replace(/\/anthropic$/, '').replace(/\/$/, '') + '/anthropic';
}
const targetProviderKey = (provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn')
? 'minimax-portal'
: provider.type;
await setOpenClawDefaultModelWithOverride(targetProviderKey, getProviderModelRef(provider), {
baseUrl,
api,
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
apiKeyEnv: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
}, fallbackModels);
logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);
try {
const defaultModelId = provider.model?.split('/').pop();
await updateAgentModelProvider(targetProviderKey, {
baseUrl,
api,
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
apiKey: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
models: defaultModelId ? [{ id: defaultModelId, name: defaultModelId }] : [],
});
} catch (err) {
logger.warn(`Failed to update models.json for OAuth provider "${targetProviderKey}":`, err);
}
}
if (
(provider.type === 'custom' || provider.type === 'ollama') &&
providerKey &&
provider.baseUrl
) {
const modelId = provider.model;
await updateAgentModelProvider(ock, {
baseUrl: provider.baseUrl,
api: 'openai-completions',
models: modelId ? [{ id: modelId, name: modelId }] : [],
apiKey: providerKey,
});
}
scheduleGatewayRestart(
gatewayManager,
`Scheduling Gateway restart after provider switch to "${ock}"`,
{ onlyIfRunning: true },
);
}