fallback model/providers (#259)
Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
bc47b455b5
commit
e52916a7ef
@@ -16,6 +16,7 @@ import {
|
||||
hasApiKey,
|
||||
saveProvider,
|
||||
getProvider,
|
||||
getAllProviders,
|
||||
deleteProvider,
|
||||
setDefaultProvider,
|
||||
getDefaultProvider,
|
||||
@@ -48,6 +49,7 @@ import { checkUvInstalled, installUv, setupManagedPython } from '../utils/uv-set
|
||||
import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config';
|
||||
import { whatsAppLoginManager } from '../utils/whatsapp-login';
|
||||
import { getProviderConfig } from '../utils/provider-registry';
|
||||
import { getProviderDefaultModel } from '../utils/provider-registry';
|
||||
import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
||||
import { applyProxySettings } from './proxy';
|
||||
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||
@@ -73,6 +75,54 @@ export function getOpenClawProviderKey(type: string, providerId: string): string
|
||||
return type;
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
return getProviderDefaultModel(config.type);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all IPC handlers
|
||||
*/
|
||||
@@ -1107,6 +1157,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
|
||||
// Sync the provider configuration to openclaw.json so Gateway knows about it
|
||||
try {
|
||||
const fallbackModels = await getProviderFallbackModelRefs(nextConfig);
|
||||
const meta = getProviderConfig(nextConfig.type);
|
||||
const api = nextConfig.type === 'custom' || nextConfig.type === 'ollama' ? 'openai-completions' : meta?.api;
|
||||
|
||||
@@ -1141,12 +1192,12 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
? `${ock}/${nextConfig.model}`
|
||||
: undefined;
|
||||
if (nextConfig.type !== 'custom' && nextConfig.type !== 'ollama') {
|
||||
await setOpenClawDefaultModel(nextConfig.type, modelOverride);
|
||||
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||
} else {
|
||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||
baseUrl: nextConfig.baseUrl,
|
||||
api: 'openai-completions',
|
||||
});
|
||||
}, fallbackModels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1222,6 +1273,7 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
try {
|
||||
const ock = getOpenClawProviderKey(provider.type, providerId);
|
||||
const providerKey = await getApiKey(providerId);
|
||||
const fallbackModels = await getProviderFallbackModelRefs(provider);
|
||||
|
||||
// OAuth providers (qwen-portal, minimax-portal, minimax-portal-cn) might use OAuth OR a direct API key.
|
||||
// Treat them as OAuth only if they don't have a local API key configured.
|
||||
@@ -1240,9 +1292,9 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
await setOpenClawDefaultModelWithOverride(ock, modelOverride, {
|
||||
baseUrl: provider.baseUrl,
|
||||
api: 'openai-completions',
|
||||
});
|
||||
}, fallbackModels);
|
||||
} else {
|
||||
await setOpenClawDefaultModel(provider.type, modelOverride);
|
||||
await setOpenClawDefaultModel(ock, modelOverride, fallbackModels);
|
||||
}
|
||||
|
||||
// Keep auth-profiles in sync with the default provider instance.
|
||||
@@ -1270,13 +1322,13 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
? 'minimax-portal'
|
||||
: provider.type;
|
||||
|
||||
await setOpenClawDefaultModelWithOverride(targetProviderKey, undefined, {
|
||||
await setOpenClawDefaultModelWithOverride(targetProviderKey, getProviderModelRef(provider), {
|
||||
baseUrl,
|
||||
api,
|
||||
authHeader: targetProviderKey === 'minimax-portal' ? true : undefined,
|
||||
// Relies on OpenClaw Gateway native auth-profiles syncing
|
||||
apiKeyEnv: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
||||
});
|
||||
}, fallbackModels);
|
||||
|
||||
logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);
|
||||
|
||||
|
||||
@@ -358,7 +358,11 @@ export function buildProviderEnvVars(providers: Array<{ type: string; apiKey: st
|
||||
* Update the OpenClaw config to use the given provider and model
|
||||
* Writes to ~/.openclaw/openclaw.json
|
||||
*/
|
||||
export async function setOpenClawDefaultModel(provider: string, modelOverride?: string): Promise<void> {
|
||||
export async function setOpenClawDefaultModel(
|
||||
provider: string,
|
||||
modelOverride?: string,
|
||||
fallbackModels: string[] = []
|
||||
): Promise<void> {
|
||||
const config = await readOpenClawJson();
|
||||
|
||||
const model = modelOverride || getProviderDefaultModel(provider);
|
||||
@@ -370,11 +374,17 @@ export async function setOpenClawDefaultModel(provider: string, modelOverride?:
|
||||
const modelId = model.startsWith(`${provider}/`)
|
||||
? model.slice(provider.length + 1)
|
||||
: model;
|
||||
const fallbackModelIds = fallbackModels
|
||||
.filter((fallback) => fallback.startsWith(`${provider}/`))
|
||||
.map((fallback) => fallback.slice(provider.length + 1));
|
||||
|
||||
// Set the default model for the agents
|
||||
const agents = (config.agents || {}) as Record<string, unknown>;
|
||||
const defaults = (agents.defaults || {}) as Record<string, unknown>;
|
||||
defaults.model = { primary: model };
|
||||
defaults.model = {
|
||||
primary: model,
|
||||
fallbacks: fallbackModels,
|
||||
};
|
||||
agents.defaults = defaults;
|
||||
config.agents = agents;
|
||||
|
||||
@@ -401,8 +411,10 @@ export async function setOpenClawDefaultModel(provider: string, modelOverride?:
|
||||
mergedModels.push(item);
|
||||
}
|
||||
}
|
||||
if (modelId && !mergedModels.some((m) => m.id === modelId)) {
|
||||
mergedModels.push({ id: modelId, name: modelId });
|
||||
for (const candidateModelId of [modelId, ...fallbackModelIds]) {
|
||||
if (candidateModelId && !mergedModels.some((m) => m.id === candidateModelId)) {
|
||||
mergedModels.push({ id: candidateModelId, name: candidateModelId });
|
||||
}
|
||||
}
|
||||
|
||||
const providerEntry: Record<string, unknown> = {
|
||||
@@ -500,7 +512,8 @@ export async function syncProviderConfigToOpenClaw(
|
||||
export async function setOpenClawDefaultModelWithOverride(
|
||||
provider: string,
|
||||
modelOverride: string | undefined,
|
||||
override: RuntimeProviderConfigOverride
|
||||
override: RuntimeProviderConfigOverride,
|
||||
fallbackModels: string[] = []
|
||||
): Promise<void> {
|
||||
const config = await readOpenClawJson();
|
||||
|
||||
@@ -513,10 +526,16 @@ export async function setOpenClawDefaultModelWithOverride(
|
||||
const modelId = model.startsWith(`${provider}/`)
|
||||
? model.slice(provider.length + 1)
|
||||
: model;
|
||||
const fallbackModelIds = fallbackModels
|
||||
.filter((fallback) => fallback.startsWith(`${provider}/`))
|
||||
.map((fallback) => fallback.slice(provider.length + 1));
|
||||
|
||||
const agents = (config.agents || {}) as Record<string, unknown>;
|
||||
const defaults = (agents.defaults || {}) as Record<string, unknown>;
|
||||
defaults.model = { primary: model };
|
||||
defaults.model = {
|
||||
primary: model,
|
||||
fallbacks: fallbackModels,
|
||||
};
|
||||
agents.defaults = defaults;
|
||||
config.agents = agents;
|
||||
|
||||
@@ -525,7 +544,11 @@ export async function setOpenClawDefaultModelWithOverride(
|
||||
const providers = (models.providers || {}) as Record<string, unknown>;
|
||||
|
||||
const nextModels: Array<Record<string, unknown>> = [];
|
||||
if (modelId) nextModels.push({ id: modelId, name: modelId });
|
||||
for (const candidateModelId of [modelId, ...fallbackModelIds]) {
|
||||
if (candidateModelId && !nextModels.some((entry) => entry.id === candidateModelId)) {
|
||||
nextModels.push({ id: candidateModelId, name: candidateModelId });
|
||||
}
|
||||
}
|
||||
|
||||
const nextProvider: Record<string, unknown> = {
|
||||
baseUrl: override.baseUrl,
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface ProviderConfig {
|
||||
type: ProviderType;
|
||||
baseUrl?: string;
|
||||
model?: string;
|
||||
fallbackModels?: string[];
|
||||
fallbackProviderIds?: string[];
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
Reference in New Issue
Block a user