729 lines
23 KiB
TypeScript
729 lines
23 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,
|
|
removeProviderKeyFromOpenClaw,
|
|
saveOAuthTokenToOpenClaw,
|
|
saveProviderKeyToOpenClaw,
|
|
setOpenClawDefaultModel,
|
|
setOpenClawDefaultModelWithOverride,
|
|
syncProviderConfigToOpenClaw,
|
|
updateAgentModelProvider,
|
|
updateSingleAgentModelProvider,
|
|
} from '../../utils/openclaw-auth';
|
|
import { logger } from '../../utils/logger';
|
|
import { listAgentsSnapshot } from '../../utils/agent-config';
|
|
|
|
const GOOGLE_OAUTH_RUNTIME_PROVIDER = 'google-gemini-cli';
|
|
const GOOGLE_OAUTH_DEFAULT_MODEL_REF = `${GOOGLE_OAUTH_RUNTIME_PROVIDER}/gemini-3-pro-preview`;
|
|
const OPENAI_OAUTH_RUNTIME_PROVIDER = 'openai-codex';
|
|
const OPENAI_OAUTH_DEFAULT_MODEL_REF = `${OPENAI_OAUTH_RUNTIME_PROVIDER}/gpt-5.4`;
|
|
|
|
type RuntimeProviderSyncContext = {
|
|
runtimeProviderKey: string;
|
|
meta: ReturnType<typeof getProviderConfig>;
|
|
api: string;
|
|
};
|
|
|
|
function normalizeProviderBaseUrl(
|
|
config: ProviderConfig,
|
|
baseUrl?: string,
|
|
apiProtocol?: string,
|
|
): string | undefined {
|
|
if (!baseUrl) {
|
|
return undefined;
|
|
}
|
|
|
|
const normalized = baseUrl.trim().replace(/\/+$/, '');
|
|
|
|
if (config.type === 'minimax-portal' || config.type === 'minimax-portal-cn') {
|
|
return normalized.replace(/\/v1$/, '').replace(/\/anthropic$/, '').replace(/\/$/, '') + '/anthropic';
|
|
}
|
|
|
|
if (config.type === 'custom' || config.type === 'ollama') {
|
|
const protocol = apiProtocol || config.apiProtocol || 'openai-completions';
|
|
if (protocol === 'openai-responses') {
|
|
return normalized.replace(/\/responses?$/i, '');
|
|
}
|
|
if (protocol === 'openai-completions') {
|
|
return normalized.replace(/\/chat\/completions$/i, '');
|
|
}
|
|
if (protocol === 'anthropic-messages') {
|
|
return normalized.replace(/\/v1\/messages$/i, '').replace(/\/messages$/i, '');
|
|
}
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function shouldUseExplicitDefaultOverride(config: ProviderConfig, runtimeProviderKey: string): boolean {
|
|
return Boolean(config.baseUrl || config.apiProtocol || runtimeProviderKey !== config.type);
|
|
}
|
|
|
|
export function getOpenClawProviderKey(type: string, providerId: string): string {
|
|
if (type === 'custom' || type === 'ollama') {
|
|
// If the providerId is already a runtime key (e.g. re-seeded from openclaw.json
|
|
// as "custom-XXXXXXXX"), return it directly to avoid double-hashing.
|
|
const prefix = `${type}-`;
|
|
if (providerId.startsWith(prefix)) {
|
|
const tail = providerId.slice(prefix.length);
|
|
if (tail.length === 8 && !tail.includes('-')) {
|
|
return providerId;
|
|
}
|
|
}
|
|
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 (account?.authMode === 'oauth_browser') {
|
|
if (config.type === 'google') {
|
|
return GOOGLE_OAUTH_RUNTIME_PROVIDER;
|
|
}
|
|
if (config.type === 'openai') {
|
|
return OPENAI_OAUTH_RUNTIME_PROVIDER;
|
|
}
|
|
}
|
|
return getOpenClawProviderKey(config.type, config.id);
|
|
}
|
|
|
|
async function getBrowserOAuthRuntimeProvider(config: ProviderConfig): Promise<string | null> {
|
|
const account = await getProviderAccount(config.id);
|
|
if (account?.authMode !== 'oauth_browser') {
|
|
return null;
|
|
}
|
|
|
|
const secret = await getProviderSecret(config.id);
|
|
if (secret?.type !== 'oauth') {
|
|
return null;
|
|
}
|
|
|
|
if (config.type === 'google') {
|
|
return GOOGLE_OAUTH_RUNTIME_PROVIDER;
|
|
}
|
|
if (config.type === 'openai') {
|
|
return OPENAI_OAUTH_RUNTIME_PROVIDER;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
type GatewayRefreshMode = 'reload' | 'restart';
|
|
|
|
function scheduleGatewayRefresh(
|
|
gatewayManager: GatewayManager | undefined,
|
|
message: string,
|
|
options?: { delayMs?: number; onlyIfRunning?: boolean; mode?: GatewayRefreshMode },
|
|
): void {
|
|
if (!gatewayManager) {
|
|
return;
|
|
}
|
|
|
|
if (options?.onlyIfRunning && gatewayManager.getStatus().state === 'stopped') {
|
|
return;
|
|
}
|
|
|
|
logger.info(message);
|
|
if (options?.mode === 'restart') {
|
|
gatewayManager.debouncedRestart(options?.delayMs);
|
|
return;
|
|
}
|
|
gatewayManager.debouncedReload(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.apiProtocol || (config.type === 'custom' ? '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: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl, context.api),
|
|
api: context.api,
|
|
apiKeyEnv: context.meta?.apiKeyEnv,
|
|
headers: config.headers ?? context.meta?.headers,
|
|
});
|
|
}
|
|
|
|
async function syncCustomProviderAgentModel(
|
|
config: ProviderConfig,
|
|
runtimeProviderKey: string,
|
|
apiKey: string | undefined,
|
|
): Promise<void> {
|
|
if (config.type !== 'custom') {
|
|
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: normalizeProviderBaseUrl(config, config.baseUrl, config.apiProtocol || 'openai-completions'),
|
|
api: config.apiProtocol || '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;
|
|
}
|
|
|
|
async function removeDeletedProviderFromOpenClaw(
|
|
provider: ProviderConfig,
|
|
providerId: string,
|
|
runtimeProviderKey?: string,
|
|
): Promise<void> {
|
|
const keys = new Set<string>();
|
|
if (runtimeProviderKey) {
|
|
keys.add(runtimeProviderKey);
|
|
} else {
|
|
keys.add(await resolveRuntimeProviderKey({ ...provider, id: providerId }));
|
|
}
|
|
keys.add(providerId);
|
|
|
|
for (const key of keys) {
|
|
await removeProviderFromOpenClaw(key);
|
|
}
|
|
}
|
|
|
|
function parseModelRef(modelRef: string): { providerKey: string; modelId: string } | null {
|
|
const trimmed = modelRef.trim();
|
|
const separatorIndex = trimmed.indexOf('/');
|
|
if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
providerKey: trimmed.slice(0, separatorIndex),
|
|
modelId: trimmed.slice(separatorIndex + 1),
|
|
};
|
|
}
|
|
|
|
async function buildRuntimeProviderConfigMap(): Promise<Map<string, ProviderConfig>> {
|
|
const configs = await getAllProviders();
|
|
const runtimeMap = new Map<string, ProviderConfig>();
|
|
|
|
for (const config of configs) {
|
|
const runtimeKey = await resolveRuntimeProviderKey(config);
|
|
runtimeMap.set(runtimeKey, config);
|
|
}
|
|
|
|
return runtimeMap;
|
|
}
|
|
|
|
async function buildAgentModelProviderEntry(
|
|
config: ProviderConfig,
|
|
modelId: string,
|
|
): Promise<{
|
|
baseUrl?: string;
|
|
api?: string;
|
|
models?: Array<{ id: string; name: string }>;
|
|
apiKey?: string;
|
|
authHeader?: boolean;
|
|
} | null> {
|
|
const meta = getProviderConfig(config.type);
|
|
const api = config.apiProtocol || (config.type === 'custom' ? 'openai-completions' : meta?.api);
|
|
const baseUrl = normalizeProviderBaseUrl(config, config.baseUrl || meta?.baseUrl, api);
|
|
if (!api || !baseUrl) {
|
|
return null;
|
|
}
|
|
|
|
let apiKey: string | undefined;
|
|
let authHeader: boolean | undefined;
|
|
|
|
if (config.type === 'custom') {
|
|
apiKey = (await getApiKey(config.id)) || undefined;
|
|
} else if (config.type === 'minimax-portal' || config.type === 'minimax-portal-cn') {
|
|
const accountApiKey = await getApiKey(config.id);
|
|
if (accountApiKey) {
|
|
apiKey = accountApiKey;
|
|
} else {
|
|
authHeader = true;
|
|
apiKey = 'minimax-oauth';
|
|
}
|
|
} else if (config.type === 'qwen-portal') {
|
|
const accountApiKey = await getApiKey(config.id);
|
|
if (accountApiKey) {
|
|
apiKey = accountApiKey;
|
|
} else {
|
|
apiKey = 'qwen-oauth';
|
|
}
|
|
}
|
|
|
|
return {
|
|
baseUrl,
|
|
api,
|
|
models: [{ id: modelId, name: modelId }],
|
|
apiKey,
|
|
authHeader,
|
|
};
|
|
}
|
|
|
|
async function syncAgentModelsToRuntime(agentIds?: Set<string>): Promise<void> {
|
|
const snapshot = await listAgentsSnapshot();
|
|
const runtimeProviderConfigs = await buildRuntimeProviderConfigMap();
|
|
|
|
const targets = snapshot.agents.filter((agent) => {
|
|
if (!agent.modelRef) return false;
|
|
if (!agentIds) return true;
|
|
return agentIds.has(agent.id);
|
|
});
|
|
|
|
for (const agent of targets) {
|
|
const parsed = parseModelRef(agent.modelRef || '');
|
|
if (!parsed) {
|
|
continue;
|
|
}
|
|
|
|
const providerConfig = runtimeProviderConfigs.get(parsed.providerKey);
|
|
if (!providerConfig) {
|
|
logger.warn(
|
|
`[provider-runtime] No provider account mapped to runtime key "${parsed.providerKey}" for agent "${agent.id}"`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const entry = await buildAgentModelProviderEntry(providerConfig, parsed.modelId);
|
|
if (!entry) {
|
|
continue;
|
|
}
|
|
|
|
await updateSingleAgentModelProvider(agent.id, parsed.providerKey, entry);
|
|
}
|
|
}
|
|
|
|
export async function syncAgentModelOverrideToRuntime(agentId: string): Promise<void> {
|
|
await syncAgentModelsToRuntime(new Set([agentId]));
|
|
}
|
|
|
|
export async function syncSavedProviderToRuntime(
|
|
config: ProviderConfig,
|
|
apiKey: string | undefined,
|
|
gatewayManager?: GatewayManager,
|
|
): Promise<void> {
|
|
const context = await syncProviderToRuntime(config, apiKey);
|
|
if (!context) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await syncAgentModelsToRuntime();
|
|
} catch (err) {
|
|
logger.warn('[provider-runtime] Failed to sync per-agent model registries after provider save:', err);
|
|
}
|
|
|
|
scheduleGatewayRefresh(
|
|
gatewayManager,
|
|
`Scheduling Gateway reload 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') {
|
|
if (shouldUseExplicitDefaultOverride(config, ock)) {
|
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
|
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl || context.meta?.baseUrl, context.api),
|
|
api: context.api,
|
|
apiKeyEnv: context.meta?.apiKeyEnv,
|
|
headers: config.headers ?? context.meta?.headers,
|
|
}, fallbackModels);
|
|
} else {
|
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
|
}
|
|
} else {
|
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
|
baseUrl: normalizeProviderBaseUrl(config, config.baseUrl, config.apiProtocol || 'openai-completions'),
|
|
api: config.apiProtocol || 'openai-completions',
|
|
headers: config.headers,
|
|
}, fallbackModels);
|
|
}
|
|
}
|
|
|
|
try {
|
|
await syncAgentModelsToRuntime();
|
|
} catch (err) {
|
|
logger.warn('[provider-runtime] Failed to sync per-agent model registries after provider update:', err);
|
|
}
|
|
|
|
scheduleGatewayRefresh(
|
|
gatewayManager,
|
|
`Scheduling Gateway reload 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 removeDeletedProviderFromOpenClaw(provider, providerId, ock);
|
|
|
|
scheduleGatewayRefresh(
|
|
gatewayManager,
|
|
`Scheduling Gateway restart after deleting provider "${ock}"`,
|
|
{ mode: 'restart' },
|
|
);
|
|
}
|
|
|
|
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 removeProviderKeyFromOpenClaw(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 browserOAuthRuntimeProvider = await getBrowserOAuthRuntimeProvider(provider);
|
|
const isOAuthProvider = (oauthTypes.includes(provider.type) && !providerKey) || Boolean(browserOAuthRuntimeProvider);
|
|
|
|
if (!isOAuthProvider) {
|
|
const modelOverride = provider.model
|
|
? (provider.model.startsWith(`${ock}/`) ? provider.model : `${ock}/${provider.model}`)
|
|
: undefined;
|
|
|
|
if (provider.type === 'custom') {
|
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
|
baseUrl: normalizeProviderBaseUrl(provider, provider.baseUrl, provider.apiProtocol || 'openai-completions'),
|
|
api: provider.apiProtocol || 'openai-completions',
|
|
headers: provider.headers,
|
|
}, fallbackModels);
|
|
} else if (shouldUseExplicitDefaultOverride(provider, ock)) {
|
|
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
|
baseUrl: normalizeProviderBaseUrl(
|
|
provider,
|
|
provider.baseUrl || getProviderConfig(provider.type)?.baseUrl,
|
|
provider.apiProtocol || getProviderConfig(provider.type)?.api,
|
|
),
|
|
api: provider.apiProtocol || getProviderConfig(provider.type)?.api,
|
|
apiKeyEnv: getProviderConfig(provider.type)?.apiKeyEnv,
|
|
headers: provider.headers ?? getProviderConfig(provider.type)?.headers,
|
|
}, fallbackModels);
|
|
} else {
|
|
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
|
}
|
|
|
|
if (providerKey) {
|
|
await saveProviderKeyToOpenClaw(ock, providerKey);
|
|
}
|
|
} else {
|
|
if (browserOAuthRuntimeProvider) {
|
|
const secret = await getProviderSecret(provider.id);
|
|
if (secret?.type === 'oauth') {
|
|
await saveOAuthTokenToOpenClaw(browserOAuthRuntimeProvider, {
|
|
access: secret.accessToken,
|
|
refresh: secret.refreshToken,
|
|
expires: secret.expiresAt,
|
|
email: secret.email,
|
|
projectId: secret.subject,
|
|
});
|
|
}
|
|
|
|
const defaultModelRef = browserOAuthRuntimeProvider === GOOGLE_OAUTH_RUNTIME_PROVIDER
|
|
? GOOGLE_OAUTH_DEFAULT_MODEL_REF
|
|
: OPENAI_OAUTH_DEFAULT_MODEL_REF;
|
|
const modelOverride = provider.model
|
|
? (provider.model.startsWith(`${browserOAuthRuntimeProvider}/`)
|
|
? provider.model
|
|
: `${browserOAuthRuntimeProvider}/${provider.model}`)
|
|
: defaultModelRef;
|
|
|
|
await setOpenClawDefaultModel(browserOAuthRuntimeProvider, modelOverride, fallbackModels);
|
|
logger.info(`Configured openclaw.json for browser OAuth provider "${provider.id}"`);
|
|
try {
|
|
await syncAgentModelsToRuntime();
|
|
} catch (err) {
|
|
logger.warn('[provider-runtime] Failed to sync per-agent model registries after browser OAuth switch:', err);
|
|
}
|
|
scheduleGatewayRefresh(
|
|
gatewayManager,
|
|
`Scheduling Gateway reload after provider switch to "${browserOAuthRuntimeProvider}"`,
|
|
);
|
|
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' &&
|
|
providerKey &&
|
|
provider.baseUrl
|
|
) {
|
|
const modelId = provider.model;
|
|
await updateAgentModelProvider(ock, {
|
|
baseUrl: normalizeProviderBaseUrl(provider, provider.baseUrl, provider.apiProtocol || 'openai-completions'),
|
|
api: provider.apiProtocol || 'openai-completions',
|
|
models: modelId ? [{ id: modelId, name: modelId }] : [],
|
|
apiKey: providerKey,
|
|
});
|
|
}
|
|
|
|
try {
|
|
await syncAgentModelsToRuntime();
|
|
} catch (err) {
|
|
logger.warn('[provider-runtime] Failed to sync per-agent model registries after default provider switch:', err);
|
|
}
|
|
|
|
scheduleGatewayRefresh(
|
|
gatewayManager,
|
|
`Scheduling Gateway reload after provider switch to "${ock}"`,
|
|
{ onlyIfRunning: true },
|
|
);
|
|
}
|