fix: clean up deleted provider state correctly (#696)
This commit is contained in:
committed by
GitHub
Unverified
parent
07f3c310b5
commit
9b56d80d22
@@ -6,6 +6,7 @@ import { getAllProviders, getApiKey, getDefaultProvider, getProvider } from '../
|
|||||||
import { getProviderConfig, getProviderDefaultModel } from '../../utils/provider-registry';
|
import { getProviderConfig, getProviderDefaultModel } from '../../utils/provider-registry';
|
||||||
import {
|
import {
|
||||||
removeProviderFromOpenClaw,
|
removeProviderFromOpenClaw,
|
||||||
|
removeProviderKeyFromOpenClaw,
|
||||||
saveOAuthTokenToOpenClaw,
|
saveOAuthTokenToOpenClaw,
|
||||||
saveProviderKeyToOpenClaw,
|
saveProviderKeyToOpenClaw,
|
||||||
setOpenClawDefaultModel,
|
setOpenClawDefaultModel,
|
||||||
@@ -20,7 +21,7 @@ import { listAgentsSnapshot } from '../../utils/agent-config';
|
|||||||
const GOOGLE_OAUTH_RUNTIME_PROVIDER = 'google-gemini-cli';
|
const GOOGLE_OAUTH_RUNTIME_PROVIDER = 'google-gemini-cli';
|
||||||
const GOOGLE_OAUTH_DEFAULT_MODEL_REF = `${GOOGLE_OAUTH_RUNTIME_PROVIDER}/gemini-3-pro-preview`;
|
const GOOGLE_OAUTH_DEFAULT_MODEL_REF = `${GOOGLE_OAUTH_RUNTIME_PROVIDER}/gemini-3-pro-preview`;
|
||||||
const OPENAI_OAUTH_RUNTIME_PROVIDER = 'openai-codex';
|
const OPENAI_OAUTH_RUNTIME_PROVIDER = 'openai-codex';
|
||||||
const OPENAI_OAUTH_DEFAULT_MODEL_REF = `${OPENAI_OAUTH_RUNTIME_PROVIDER}/gpt-5.3-codex`;
|
const OPENAI_OAUTH_DEFAULT_MODEL_REF = `${OPENAI_OAUTH_RUNTIME_PROVIDER}/gpt-5.4`;
|
||||||
|
|
||||||
type RuntimeProviderSyncContext = {
|
type RuntimeProviderSyncContext = {
|
||||||
runtimeProviderKey: string;
|
runtimeProviderKey: string;
|
||||||
@@ -347,6 +348,24 @@ async function syncProviderToRuntime(
|
|||||||
return context;
|
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 {
|
function parseModelRef(modelRef: string): { providerKey: string; modelId: string } | null {
|
||||||
const trimmed = modelRef.trim();
|
const trimmed = modelRef.trim();
|
||||||
const separatorIndex = trimmed.indexOf('/');
|
const separatorIndex = trimmed.indexOf('/');
|
||||||
@@ -538,7 +557,7 @@ export async function syncDeletedProviderToRuntime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ock = runtimeProviderKey ?? await resolveRuntimeProviderKey({ ...provider, id: providerId });
|
const ock = runtimeProviderKey ?? await resolveRuntimeProviderKey({ ...provider, id: providerId });
|
||||||
await removeProviderFromOpenClaw(ock);
|
await removeDeletedProviderFromOpenClaw(provider, providerId, ock);
|
||||||
|
|
||||||
scheduleGatewayRefresh(
|
scheduleGatewayRefresh(
|
||||||
gatewayManager,
|
gatewayManager,
|
||||||
@@ -557,7 +576,7 @@ export async function syncDeletedProviderApiKeyToRuntime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ock = runtimeProviderKey ?? await resolveRuntimeProviderKey({ ...provider, id: providerId });
|
const ock = runtimeProviderKey ?? await resolveRuntimeProviderKey({ ...provider, id: providerId });
|
||||||
await removeProviderFromOpenClaw(ock);
|
await removeProviderKeyFromOpenClaw(ock);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncDefaultProviderToRuntime(
|
export async function syncDefaultProviderToRuntime(
|
||||||
|
|||||||
@@ -29,9 +29,12 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
|
|||||||
requiresApiKey: true,
|
requiresApiKey: true,
|
||||||
category: 'official',
|
category: 'official',
|
||||||
envVar: 'OPENAI_API_KEY',
|
envVar: 'OPENAI_API_KEY',
|
||||||
defaultModelId: 'gpt-5.2',
|
defaultModelId: 'gpt-5.4',
|
||||||
isOAuth: true,
|
isOAuth: true,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
|
showModelId: true,
|
||||||
|
showModelIdInDevModeOnly: true,
|
||||||
|
modelIdPlaceholder: 'gpt-5.4',
|
||||||
supportedAuthModes: ['api_key', 'oauth_browser'],
|
supportedAuthModes: ['api_key', 'oauth_browser'],
|
||||||
defaultAuthMode: 'api_key',
|
defaultAuthMode: 'api_key',
|
||||||
supportsMultipleAccounts: true,
|
supportsMultipleAccounts: true,
|
||||||
@@ -50,9 +53,12 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
|
|||||||
requiresApiKey: true,
|
requiresApiKey: true,
|
||||||
category: 'official',
|
category: 'official',
|
||||||
envVar: 'GEMINI_API_KEY',
|
envVar: 'GEMINI_API_KEY',
|
||||||
defaultModelId: 'gemini-3.1-pro-preview',
|
defaultModelId: 'gemini-3-pro-preview',
|
||||||
isOAuth: true,
|
isOAuth: true,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
|
showModelId: true,
|
||||||
|
showModelIdInDevModeOnly: true,
|
||||||
|
modelIdPlaceholder: 'gemini-3-pro-preview',
|
||||||
supportedAuthModes: ['api_key', 'oauth_browser'],
|
supportedAuthModes: ['api_key', 'oauth_browser'],
|
||||||
defaultAuthMode: 'api_key',
|
defaultAuthMode: 'api_key',
|
||||||
supportsMultipleAccounts: true,
|
supportsMultipleAccounts: true,
|
||||||
@@ -171,6 +177,9 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
|
|||||||
isOAuth: true,
|
isOAuth: true,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
defaultModelId: 'MiniMax-M2.7',
|
defaultModelId: 'MiniMax-M2.7',
|
||||||
|
showModelId: true,
|
||||||
|
showModelIdInDevModeOnly: true,
|
||||||
|
modelIdPlaceholder: 'MiniMax-M2.7',
|
||||||
apiKeyUrl: 'https://platform.minimax.io',
|
apiKeyUrl: 'https://platform.minimax.io',
|
||||||
category: 'official',
|
category: 'official',
|
||||||
envVar: 'MINIMAX_API_KEY',
|
envVar: 'MINIMAX_API_KEY',
|
||||||
@@ -193,6 +202,9 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
|
|||||||
isOAuth: true,
|
isOAuth: true,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
defaultModelId: 'MiniMax-M2.7',
|
defaultModelId: 'MiniMax-M2.7',
|
||||||
|
showModelId: true,
|
||||||
|
showModelIdInDevModeOnly: true,
|
||||||
|
modelIdPlaceholder: 'MiniMax-M2.7',
|
||||||
apiKeyUrl: 'https://platform.minimaxi.com/',
|
apiKeyUrl: 'https://platform.minimaxi.com/',
|
||||||
category: 'official',
|
category: 'official',
|
||||||
envVar: 'MINIMAX_CN_API_KEY',
|
envVar: 'MINIMAX_CN_API_KEY',
|
||||||
@@ -214,6 +226,9 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
|
|||||||
requiresApiKey: false,
|
requiresApiKey: false,
|
||||||
isOAuth: true,
|
isOAuth: true,
|
||||||
defaultModelId: 'coder-model',
|
defaultModelId: 'coder-model',
|
||||||
|
showModelId: true,
|
||||||
|
showModelIdInDevModeOnly: true,
|
||||||
|
modelIdPlaceholder: 'coder-model',
|
||||||
category: 'official',
|
category: 'official',
|
||||||
envVar: 'QWEN_API_KEY',
|
envVar: 'QWEN_API_KEY',
|
||||||
supportedAuthModes: ['oauth_device'],
|
supportedAuthModes: ['oauth_device'],
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type BrowserOAuthProviderType = 'google' | 'openai';
|
|||||||
const GOOGLE_RUNTIME_PROVIDER_ID = 'google-gemini-cli';
|
const GOOGLE_RUNTIME_PROVIDER_ID = 'google-gemini-cli';
|
||||||
const GOOGLE_OAUTH_DEFAULT_MODEL = 'gemini-3-pro-preview';
|
const GOOGLE_OAUTH_DEFAULT_MODEL = 'gemini-3-pro-preview';
|
||||||
const OPENAI_RUNTIME_PROVIDER_ID = 'openai-codex';
|
const OPENAI_RUNTIME_PROVIDER_ID = 'openai-codex';
|
||||||
const OPENAI_OAUTH_DEFAULT_MODEL = 'gpt-5.3-codex';
|
const OPENAI_OAUTH_DEFAULT_MODEL = 'gpt-5.4';
|
||||||
|
|
||||||
class BrowserOAuthManager extends EventEmitter {
|
class BrowserOAuthManager extends EventEmitter {
|
||||||
private activeProvider: BrowserOAuthProviderType | null = null;
|
private activeProvider: BrowserOAuthProviderType | null = null;
|
||||||
|
|||||||
@@ -93,6 +93,82 @@ interface AuthProfilesStore {
|
|||||||
lastGood?: Record<string, string>;
|
lastGood?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeProfilesForProvider(store: AuthProfilesStore, provider: string): boolean {
|
||||||
|
const removedProfileIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const [profileId, profile] of Object.entries(store.profiles)) {
|
||||||
|
if (profile?.provider !== provider) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
delete store.profiles[profileId];
|
||||||
|
removedProfileIds.add(profileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedProfileIds.size === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.order) {
|
||||||
|
for (const [orderProvider, profileIds] of Object.entries(store.order)) {
|
||||||
|
const nextProfileIds = profileIds.filter((profileId) => !removedProfileIds.has(profileId));
|
||||||
|
if (nextProfileIds.length > 0) {
|
||||||
|
store.order[orderProvider] = nextProfileIds;
|
||||||
|
} else {
|
||||||
|
delete store.order[orderProvider];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.lastGood) {
|
||||||
|
for (const [lastGoodProvider, profileId] of Object.entries(store.lastGood)) {
|
||||||
|
if (removedProfileIds.has(profileId)) {
|
||||||
|
delete store.lastGood[lastGoodProvider];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeProfileFromStore(
|
||||||
|
store: AuthProfilesStore,
|
||||||
|
profileId: string,
|
||||||
|
expectedType?: AuthProfileEntry['type'] | OAuthProfileEntry['type'],
|
||||||
|
): boolean {
|
||||||
|
const profile = store.profiles[profileId];
|
||||||
|
let changed = false;
|
||||||
|
const shouldCleanReferences = !profile || !expectedType || profile.type === expectedType;
|
||||||
|
if (profile && (!expectedType || profile.type === expectedType)) {
|
||||||
|
delete store.profiles[profileId];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCleanReferences && store.order) {
|
||||||
|
for (const [orderProvider, profileIds] of Object.entries(store.order)) {
|
||||||
|
const nextProfileIds = profileIds.filter((id) => id !== profileId);
|
||||||
|
if (nextProfileIds.length !== profileIds.length) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (nextProfileIds.length > 0) {
|
||||||
|
store.order[orderProvider] = nextProfileIds;
|
||||||
|
} else {
|
||||||
|
delete store.order[orderProvider];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCleanReferences && store.lastGood) {
|
||||||
|
for (const [lastGoodProvider, lastGoodProfileId] of Object.entries(store.lastGood)) {
|
||||||
|
if (lastGoodProfileId === profileId) {
|
||||||
|
delete store.lastGood[lastGoodProvider];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Auth Profiles I/O ────────────────────────────────────────────
|
// ── Auth Profiles I/O ────────────────────────────────────────────
|
||||||
|
|
||||||
function getAuthProfilesPath(agentId = 'main'): string {
|
function getAuthProfilesPath(agentId = 'main'): string {
|
||||||
@@ -346,27 +422,15 @@ export async function removeProviderKeyFromOpenClaw(
|
|||||||
provider: string,
|
provider: string,
|
||||||
agentId?: string
|
agentId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (isOAuthProviderType(provider)) {
|
|
||||||
console.log(`Skipping auth-profiles removal for OAuth provider "${provider}" (managed by OpenClaw plugin)`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const agentIds = agentId ? [agentId] : await discoverAgentIds();
|
const agentIds = agentId ? [agentId] : await discoverAgentIds();
|
||||||
if (agentIds.length === 0) agentIds.push('main');
|
if (agentIds.length === 0) agentIds.push('main');
|
||||||
|
|
||||||
for (const id of agentIds) {
|
for (const id of agentIds) {
|
||||||
const store = await readAuthProfiles(id);
|
const store = await readAuthProfiles(id);
|
||||||
const profileId = `${provider}:default`;
|
if (removeProfileFromStore(store, `${provider}:default`, 'api_key')) {
|
||||||
|
|
||||||
delete store.profiles[profileId];
|
|
||||||
|
|
||||||
if (store.order?.[provider]) {
|
|
||||||
store.order[provider] = store.order[provider].filter((aid) => aid !== profileId);
|
|
||||||
if (store.order[provider].length === 0) delete store.order[provider];
|
|
||||||
}
|
|
||||||
if (store.lastGood?.[provider] === profileId) delete store.lastGood[provider];
|
|
||||||
|
|
||||||
await writeAuthProfiles(store, id);
|
await writeAuthProfiles(store, id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
console.log(`Removed API key for provider "${provider}" from OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`);
|
console.log(`Removed API key for provider "${provider}" from OpenClaw auth-profiles (agents: ${agentIds.join(', ')})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,14 +443,7 @@ export async function removeProviderFromOpenClaw(provider: string): Promise<void
|
|||||||
if (agentIds.length === 0) agentIds.push('main');
|
if (agentIds.length === 0) agentIds.push('main');
|
||||||
for (const id of agentIds) {
|
for (const id of agentIds) {
|
||||||
const store = await readAuthProfiles(id);
|
const store = await readAuthProfiles(id);
|
||||||
const profileId = `${provider}:default`;
|
if (removeProfilesForProvider(store, provider)) {
|
||||||
if (store.profiles[profileId]) {
|
|
||||||
delete store.profiles[profileId];
|
|
||||||
if (store.order?.[provider]) {
|
|
||||||
store.order[provider] = store.order[provider].filter((aid) => aid !== profileId);
|
|
||||||
if (store.order[provider].length === 0) delete store.order[provider];
|
|
||||||
}
|
|
||||||
if (store.lastGood?.[provider] === profileId) delete store.lastGood[provider];
|
|
||||||
await writeAuthProfiles(store, id);
|
await writeAuthProfiles(store, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,6 +492,25 @@ export async function removeProviderFromOpenClaw(provider: string): Promise<void
|
|||||||
console.log(`Removed OpenClaw provider config: ${provider}`);
|
console.log(`Removed OpenClaw provider config: ${provider}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auth = (config.auth && typeof config.auth === 'object'
|
||||||
|
? config.auth as Record<string, unknown>
|
||||||
|
: null);
|
||||||
|
const authProfiles = (
|
||||||
|
auth?.profiles && typeof auth.profiles === 'object'
|
||||||
|
? auth.profiles as Record<string, AuthProfileEntry | OAuthProfileEntry>
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
if (authProfiles) {
|
||||||
|
for (const [profileId, profile] of Object.entries(authProfiles)) {
|
||||||
|
if (profile?.provider !== provider) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
delete authProfiles[profileId];
|
||||||
|
modified = true;
|
||||||
|
console.log(`Removed OpenClaw auth profile: ${profileId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up agents.defaults.model references that point to the deleted provider.
|
// Clean up agents.defaults.model references that point to the deleted provider.
|
||||||
// Model refs use the format "providerType/modelId", e.g. "openai/gpt-4".
|
// Model refs use the format "providerType/modelId", e.g. "openai/gpt-4".
|
||||||
// Leaving stale refs causes the Gateway to report "Unknown model" errors.
|
// Leaving stale refs causes the Gateway to report "Unknown model" errors.
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
|
|||||||
requiresApiKey: true,
|
requiresApiKey: true,
|
||||||
isOAuth: true,
|
isOAuth: true,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
|
defaultModelId: 'gpt-5.4',
|
||||||
|
showModelId: true,
|
||||||
|
showModelIdInDevModeOnly: true,
|
||||||
|
modelIdPlaceholder: 'gpt-5.4',
|
||||||
apiKeyUrl: 'https://platform.openai.com/api-keys',
|
apiKeyUrl: 'https://platform.openai.com/api-keys',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -157,15 +161,18 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
|
|||||||
requiresApiKey: true,
|
requiresApiKey: true,
|
||||||
isOAuth: true,
|
isOAuth: true,
|
||||||
supportsApiKey: true,
|
supportsApiKey: true,
|
||||||
defaultModelId: 'gemini-3.1-pro-preview',
|
defaultModelId: 'gemini-3-pro-preview',
|
||||||
|
showModelId: true,
|
||||||
|
showModelIdInDevModeOnly: true,
|
||||||
|
modelIdPlaceholder: 'gemini-3-pro-preview',
|
||||||
apiKeyUrl: 'https://aistudio.google.com/app/apikey',
|
apiKeyUrl: 'https://aistudio.google.com/app/apikey',
|
||||||
},
|
},
|
||||||
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, modelIdPlaceholder: 'openai/gpt-5.4', defaultModelId: 'openai/gpt-5.4', docsUrl: 'https://openrouter.ai/models' },
|
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, modelIdPlaceholder: 'openai/gpt-5.4', defaultModelId: 'openai/gpt-5.4', docsUrl: 'https://openrouter.ai/models' },
|
||||||
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimaxi.com/' },
|
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.7', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimaxi.com/' },
|
||||||
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5', docsUrl: 'https://platform.moonshot.cn/' },
|
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5', docsUrl: 'https://platform.moonshot.cn/' },
|
||||||
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', showModelId: true, modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3', defaultModelId: 'deepseek-ai/DeepSeek-V3', docsUrl: 'https://docs.siliconflow.cn/cn/userguide/introduction' },
|
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', showModelId: true, modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3', defaultModelId: 'deepseek-ai/DeepSeek-V3', docsUrl: 'https://docs.siliconflow.cn/cn/userguide/introduction' },
|
||||||
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimax.io' },
|
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.7', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimax.io' },
|
||||||
{ id: 'qwen-portal', name: 'Qwen (Global)', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model' },
|
{ id: 'qwen-portal', name: 'Qwen (Global)', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'coder-model' },
|
||||||
{ 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', docsUrl: 'https://www.volcengine.com/', codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', codePlanPresetModelId: 'ark-code-latest', codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh' },
|
{ 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', docsUrl: 'https://www.volcengine.com/', codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', codePlanPresetModelId: 'ark-code-latest', codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh' },
|
||||||
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },
|
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ async function writeOpenClawJson(config: unknown): Promise<void> {
|
|||||||
await writeFile(join(openclawDir, 'openclaw.json'), JSON.stringify(config, null, 2), 'utf8');
|
await writeFile(join(openclawDir, 'openclaw.json'), JSON.stringify(config, null, 2), 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readOpenClawJson(): Promise<Record<string, unknown>> {
|
||||||
|
const content = await readFile(join(testHome, '.openclaw', 'openclaw.json'), 'utf8');
|
||||||
|
return JSON.parse(content) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
async function readAuthProfiles(agentId: string): Promise<Record<string, unknown>> {
|
async function readAuthProfiles(agentId: string): Promise<Record<string, unknown>> {
|
||||||
const content = await readFile(join(testHome, '.openclaw', 'agents', agentId, 'agent', 'auth-profiles.json'), 'utf8');
|
const content = await readFile(join(testHome, '.openclaw', 'agents', agentId, 'agent', 'auth-profiles.json'), 'utf8');
|
||||||
return JSON.parse(content) as Record<string, unknown>;
|
return JSON.parse(content) as Record<string, unknown>;
|
||||||
@@ -118,6 +123,188 @@ describe('saveProviderKeyToOpenClaw', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('removeProviderKeyFromOpenClaw', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
await rm(testHome, { recursive: true, force: true });
|
||||||
|
await rm(testUserData, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes only the default api-key profile for a provider', async () => {
|
||||||
|
await writeAgentAuthProfiles('main', {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
'custom-abc12345:default': {
|
||||||
|
type: 'api_key',
|
||||||
|
provider: 'custom-abc12345',
|
||||||
|
key: 'sk-main',
|
||||||
|
},
|
||||||
|
'custom-abc12345:backup': {
|
||||||
|
type: 'api_key',
|
||||||
|
provider: 'custom-abc12345',
|
||||||
|
key: 'sk-backup',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
'custom-abc12345': [
|
||||||
|
'custom-abc12345:default',
|
||||||
|
'custom-abc12345:backup',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lastGood: {
|
||||||
|
'custom-abc12345': 'custom-abc12345:default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { removeProviderKeyFromOpenClaw } = await import('@electron/utils/openclaw-auth');
|
||||||
|
|
||||||
|
await removeProviderKeyFromOpenClaw('custom-abc12345', 'main');
|
||||||
|
|
||||||
|
const mainProfiles = await readAuthProfiles('main');
|
||||||
|
expect(mainProfiles.profiles).toEqual({
|
||||||
|
'custom-abc12345:backup': {
|
||||||
|
type: 'api_key',
|
||||||
|
provider: 'custom-abc12345',
|
||||||
|
key: 'sk-backup',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(mainProfiles.order).toEqual({
|
||||||
|
'custom-abc12345': ['custom-abc12345:backup'],
|
||||||
|
});
|
||||||
|
expect(mainProfiles.lastGood).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleans stale default-profile references even when the profile object is already missing', async () => {
|
||||||
|
await writeAgentAuthProfiles('main', {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
'custom-abc12345:backup': {
|
||||||
|
type: 'api_key',
|
||||||
|
provider: 'custom-abc12345',
|
||||||
|
key: 'sk-backup',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
'custom-abc12345': [
|
||||||
|
'custom-abc12345:default',
|
||||||
|
'custom-abc12345:backup',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lastGood: {
|
||||||
|
'custom-abc12345': 'custom-abc12345:default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { removeProviderKeyFromOpenClaw } = await import('@electron/utils/openclaw-auth');
|
||||||
|
|
||||||
|
await removeProviderKeyFromOpenClaw('custom-abc12345', 'main');
|
||||||
|
|
||||||
|
const mainProfiles = await readAuthProfiles('main');
|
||||||
|
expect(mainProfiles.profiles).toEqual({
|
||||||
|
'custom-abc12345:backup': {
|
||||||
|
type: 'api_key',
|
||||||
|
provider: 'custom-abc12345',
|
||||||
|
key: 'sk-backup',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(mainProfiles.order).toEqual({
|
||||||
|
'custom-abc12345': ['custom-abc12345:backup'],
|
||||||
|
});
|
||||||
|
expect(mainProfiles.lastGood).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not remove oauth default profiles when deleting only an api key', async () => {
|
||||||
|
await writeAgentAuthProfiles('main', {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
'openai-codex:default': {
|
||||||
|
type: 'oauth',
|
||||||
|
provider: 'openai-codex',
|
||||||
|
access: 'acc',
|
||||||
|
refresh: 'ref',
|
||||||
|
expires: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
'openai-codex': ['openai-codex:default'],
|
||||||
|
},
|
||||||
|
lastGood: {
|
||||||
|
'openai-codex': 'openai-codex:default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { removeProviderKeyFromOpenClaw } = await import('@electron/utils/openclaw-auth');
|
||||||
|
|
||||||
|
await removeProviderKeyFromOpenClaw('openai-codex', 'main');
|
||||||
|
|
||||||
|
const mainProfiles = await readAuthProfiles('main');
|
||||||
|
expect(mainProfiles.profiles).toEqual({
|
||||||
|
'openai-codex:default': {
|
||||||
|
type: 'oauth',
|
||||||
|
provider: 'openai-codex',
|
||||||
|
access: 'acc',
|
||||||
|
refresh: 'ref',
|
||||||
|
expires: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(mainProfiles.order).toEqual({
|
||||||
|
'openai-codex': ['openai-codex:default'],
|
||||||
|
});
|
||||||
|
expect(mainProfiles.lastGood).toEqual({
|
||||||
|
'openai-codex': 'openai-codex:default',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes api-key defaults for oauth-capable providers that support api keys', async () => {
|
||||||
|
await writeAgentAuthProfiles('main', {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
'minimax-portal:default': {
|
||||||
|
type: 'api_key',
|
||||||
|
provider: 'minimax-portal',
|
||||||
|
key: 'sk-minimax',
|
||||||
|
},
|
||||||
|
'minimax-portal:oauth-backup': {
|
||||||
|
type: 'oauth',
|
||||||
|
provider: 'minimax-portal',
|
||||||
|
access: 'acc',
|
||||||
|
refresh: 'ref',
|
||||||
|
expires: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
'minimax-portal': [
|
||||||
|
'minimax-portal:default',
|
||||||
|
'minimax-portal:oauth-backup',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lastGood: {
|
||||||
|
'minimax-portal': 'minimax-portal:default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { removeProviderKeyFromOpenClaw } = await import('@electron/utils/openclaw-auth');
|
||||||
|
|
||||||
|
await removeProviderKeyFromOpenClaw('minimax-portal', 'main');
|
||||||
|
|
||||||
|
const mainProfiles = await readAuthProfiles('main');
|
||||||
|
expect(mainProfiles.profiles).toEqual({
|
||||||
|
'minimax-portal:oauth-backup': {
|
||||||
|
type: 'oauth',
|
||||||
|
provider: 'minimax-portal',
|
||||||
|
access: 'acc',
|
||||||
|
refresh: 'ref',
|
||||||
|
expires: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(mainProfiles.order).toEqual({
|
||||||
|
'minimax-portal': ['minimax-portal:oauth-backup'],
|
||||||
|
});
|
||||||
|
expect(mainProfiles.lastGood).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('sanitizeOpenClawConfig', () => {
|
describe('sanitizeOpenClawConfig', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
@@ -292,4 +479,86 @@ describe('auth-backed provider discovery', () => {
|
|||||||
anthropic: {},
|
anthropic: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removes all matching auth profiles for a deleted provider so it does not reappear', async () => {
|
||||||
|
await writeOpenClawJson({
|
||||||
|
agents: {
|
||||||
|
list: [
|
||||||
|
{ id: 'main', name: 'Main', default: true, workspace: '~/.openclaw/workspace', agentDir: '~/.openclaw/agents/main/agent' },
|
||||||
|
{ id: 'work', name: 'Work', workspace: '~/.openclaw/workspace-work', agentDir: '~/.openclaw/agents/work/agent' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
'custom-abc12345': {
|
||||||
|
baseUrl: 'https://api.moonshot.cn/v1',
|
||||||
|
api: 'openai-completions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
profiles: {
|
||||||
|
'custom-abc12345:oauth': {
|
||||||
|
type: 'oauth',
|
||||||
|
provider: 'custom-abc12345',
|
||||||
|
access: 'acc',
|
||||||
|
refresh: 'ref',
|
||||||
|
expires: 1,
|
||||||
|
},
|
||||||
|
'custom-abc12345:secondary': {
|
||||||
|
type: 'api_key',
|
||||||
|
provider: 'custom-abc12345',
|
||||||
|
key: 'sk-inline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeAgentAuthProfiles('main', {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
'custom-abc12345:default': {
|
||||||
|
type: 'api_key',
|
||||||
|
provider: 'custom-abc12345',
|
||||||
|
key: 'sk-main',
|
||||||
|
},
|
||||||
|
'custom-abc12345:backup': {
|
||||||
|
type: 'api_key',
|
||||||
|
provider: 'custom-abc12345',
|
||||||
|
key: 'sk-backup',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
'custom-abc12345': [
|
||||||
|
'custom-abc12345:default',
|
||||||
|
'custom-abc12345:backup',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lastGood: {
|
||||||
|
'custom-abc12345': 'custom-abc12345:backup',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
getActiveOpenClawProviders,
|
||||||
|
getOpenClawProvidersConfig,
|
||||||
|
removeProviderFromOpenClaw,
|
||||||
|
} = await import('@electron/utils/openclaw-auth');
|
||||||
|
|
||||||
|
await expect(getActiveOpenClawProviders()).resolves.toEqual(new Set(['custom-abc12345']));
|
||||||
|
|
||||||
|
await removeProviderFromOpenClaw('custom-abc12345');
|
||||||
|
|
||||||
|
const mainProfiles = await readAuthProfiles('main');
|
||||||
|
const config = await readOpenClawJson();
|
||||||
|
const result = await getOpenClawProvidersConfig();
|
||||||
|
|
||||||
|
expect(mainProfiles.profiles).toEqual({});
|
||||||
|
expect(mainProfiles.order).toEqual({});
|
||||||
|
expect(mainProfiles.lastGood).toEqual({});
|
||||||
|
expect((config.auth as { profiles?: Record<string, unknown> }).profiles).toEqual({});
|
||||||
|
expect((config.models as { providers?: Record<string, unknown> }).providers).toEqual({});
|
||||||
|
expect(result.providers).toEqual({});
|
||||||
|
await expect(getActiveOpenClawProviders()).resolves.toEqual(new Set());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const mocks = vi.hoisted(() => ({
|
|||||||
getProviderConfig: vi.fn(),
|
getProviderConfig: vi.fn(),
|
||||||
getProviderDefaultModel: vi.fn(),
|
getProviderDefaultModel: vi.fn(),
|
||||||
removeProviderFromOpenClaw: vi.fn(),
|
removeProviderFromOpenClaw: vi.fn(),
|
||||||
|
removeProviderKeyFromOpenClaw: vi.fn(),
|
||||||
saveOAuthTokenToOpenClaw: vi.fn(),
|
saveOAuthTokenToOpenClaw: vi.fn(),
|
||||||
saveProviderKeyToOpenClaw: vi.fn(),
|
saveProviderKeyToOpenClaw: vi.fn(),
|
||||||
setOpenClawDefaultModel: vi.fn(),
|
setOpenClawDefaultModel: vi.fn(),
|
||||||
@@ -46,6 +47,7 @@ vi.mock('@electron/utils/provider-registry', () => ({
|
|||||||
|
|
||||||
vi.mock('@electron/utils/openclaw-auth', () => ({
|
vi.mock('@electron/utils/openclaw-auth', () => ({
|
||||||
removeProviderFromOpenClaw: mocks.removeProviderFromOpenClaw,
|
removeProviderFromOpenClaw: mocks.removeProviderFromOpenClaw,
|
||||||
|
removeProviderKeyFromOpenClaw: mocks.removeProviderKeyFromOpenClaw,
|
||||||
saveOAuthTokenToOpenClaw: mocks.saveOAuthTokenToOpenClaw,
|
saveOAuthTokenToOpenClaw: mocks.saveOAuthTokenToOpenClaw,
|
||||||
saveProviderKeyToOpenClaw: mocks.saveProviderKeyToOpenClaw,
|
saveProviderKeyToOpenClaw: mocks.saveProviderKeyToOpenClaw,
|
||||||
setOpenClawDefaultModel: mocks.setOpenClawDefaultModel,
|
setOpenClawDefaultModel: mocks.setOpenClawDefaultModel,
|
||||||
@@ -71,6 +73,7 @@ vi.mock('@electron/utils/logger', () => ({
|
|||||||
import {
|
import {
|
||||||
syncAgentModelOverrideToRuntime,
|
syncAgentModelOverrideToRuntime,
|
||||||
syncDefaultProviderToRuntime,
|
syncDefaultProviderToRuntime,
|
||||||
|
syncDeletedProviderApiKeyToRuntime,
|
||||||
syncDeletedProviderToRuntime,
|
syncDeletedProviderToRuntime,
|
||||||
syncSavedProviderToRuntime,
|
syncSavedProviderToRuntime,
|
||||||
} from '@electron/services/providers/provider-runtime-sync';
|
} from '@electron/services/providers/provider-runtime-sync';
|
||||||
@@ -116,6 +119,7 @@ describe('provider-runtime-sync refresh strategy', () => {
|
|||||||
mocks.setOpenClawDefaultModelWithOverride.mockResolvedValue(undefined);
|
mocks.setOpenClawDefaultModelWithOverride.mockResolvedValue(undefined);
|
||||||
mocks.saveProviderKeyToOpenClaw.mockResolvedValue(undefined);
|
mocks.saveProviderKeyToOpenClaw.mockResolvedValue(undefined);
|
||||||
mocks.removeProviderFromOpenClaw.mockResolvedValue(undefined);
|
mocks.removeProviderFromOpenClaw.mockResolvedValue(undefined);
|
||||||
|
mocks.removeProviderKeyFromOpenClaw.mockResolvedValue(undefined);
|
||||||
mocks.updateAgentModelProvider.mockResolvedValue(undefined);
|
mocks.updateAgentModelProvider.mockResolvedValue(undefined);
|
||||||
mocks.updateSingleAgentModelProvider.mockResolvedValue(undefined);
|
mocks.updateSingleAgentModelProvider.mockResolvedValue(undefined);
|
||||||
mocks.listAgentsSnapshot.mockResolvedValue({ agents: [] });
|
mocks.listAgentsSnapshot.mockResolvedValue({ agents: [] });
|
||||||
@@ -137,6 +141,34 @@ describe('provider-runtime-sync refresh strategy', () => {
|
|||||||
expect(gateway.debouncedReload).not.toHaveBeenCalled();
|
expect(gateway.debouncedReload).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removes both runtime and stored account keys when deleting a custom provider', async () => {
|
||||||
|
const gateway = createGateway('running');
|
||||||
|
const customProvider = createProvider({
|
||||||
|
id: 'moonshot-cn',
|
||||||
|
type: 'custom',
|
||||||
|
baseUrl: 'https://api.moonshot.cn/v1',
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncDeletedProviderToRuntime(customProvider, 'moonshot-cn', gateway as GatewayManager);
|
||||||
|
|
||||||
|
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledWith('custom-moonshot');
|
||||||
|
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledWith('moonshot-cn');
|
||||||
|
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledTimes(2);
|
||||||
|
expect(gateway.debouncedRestart).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only clears the api-key profile when deleting a provider api key', async () => {
|
||||||
|
const openaiProvider = createProvider({
|
||||||
|
id: 'openai-personal',
|
||||||
|
type: 'openai',
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncDeletedProviderApiKeyToRuntime(openaiProvider, 'openai-personal');
|
||||||
|
|
||||||
|
expect(mocks.removeProviderKeyFromOpenClaw).toHaveBeenCalledWith('openai');
|
||||||
|
expect(mocks.removeProviderFromOpenClaw).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('uses debouncedReload after switching default provider when gateway is running', async () => {
|
it('uses debouncedReload after switching default provider when gateway is running', async () => {
|
||||||
const gateway = createGateway('running');
|
const gateway = createGateway('running');
|
||||||
await syncDefaultProviderToRuntime('moonshot', gateway as GatewayManager);
|
await syncDefaultProviderToRuntime('moonshot', gateway as GatewayManager);
|
||||||
@@ -153,6 +185,34 @@ describe('provider-runtime-sync refresh strategy', () => {
|
|||||||
expect(gateway.debouncedRestart).not.toHaveBeenCalled();
|
expect(gateway.debouncedRestart).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses gpt-5.4 as the browser OAuth default model for OpenAI', async () => {
|
||||||
|
mocks.getProvider.mockResolvedValue(
|
||||||
|
createProvider({
|
||||||
|
id: 'openai-personal',
|
||||||
|
type: 'openai',
|
||||||
|
model: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
mocks.getProviderAccount.mockResolvedValue({ authMode: 'oauth_browser' });
|
||||||
|
mocks.getProviderSecret.mockResolvedValue({
|
||||||
|
type: 'oauth',
|
||||||
|
accessToken: 'access',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: 123,
|
||||||
|
email: 'user@example.com',
|
||||||
|
subject: 'project-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const gateway = createGateway('running');
|
||||||
|
await syncDefaultProviderToRuntime('openai-personal', gateway as GatewayManager);
|
||||||
|
|
||||||
|
expect(mocks.setOpenClawDefaultModel).toHaveBeenCalledWith(
|
||||||
|
'openai-codex',
|
||||||
|
'openai-codex/gpt-5.4',
|
||||||
|
expect.any(Array),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('syncs a targeted agent model override to runtime provider registry', async () => {
|
it('syncs a targeted agent model override to runtime provider registry', async () => {
|
||||||
mocks.getAllProviders.mockResolvedValue([
|
mocks.getAllProviders.mockResolvedValue([
|
||||||
createProvider({
|
createProvider({
|
||||||
|
|||||||
@@ -119,6 +119,38 @@ describe('provider metadata', () => {
|
|||||||
expect(shouldShowProviderModelId(siliconflow, true)).toBe(true);
|
expect(shouldShowProviderModelId(siliconflow, true)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows OAuth model overrides only in dev mode and preserves defaults', () => {
|
||||||
|
const openai = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openai');
|
||||||
|
const google = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'google');
|
||||||
|
const minimax = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'minimax-portal');
|
||||||
|
const minimaxCn = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'minimax-portal-cn');
|
||||||
|
const qwen = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'qwen-portal');
|
||||||
|
|
||||||
|
expect(openai).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'gpt-5.4' });
|
||||||
|
expect(google).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'gemini-3-pro-preview' });
|
||||||
|
expect(minimax).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'MiniMax-M2.7' });
|
||||||
|
expect(minimaxCn).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'MiniMax-M2.7' });
|
||||||
|
expect(qwen).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'coder-model' });
|
||||||
|
|
||||||
|
expect(shouldShowProviderModelId(openai, false)).toBe(false);
|
||||||
|
expect(shouldShowProviderModelId(google, false)).toBe(false);
|
||||||
|
expect(shouldShowProviderModelId(minimax, false)).toBe(false);
|
||||||
|
expect(shouldShowProviderModelId(minimaxCn, false)).toBe(false);
|
||||||
|
expect(shouldShowProviderModelId(qwen, false)).toBe(false);
|
||||||
|
|
||||||
|
expect(shouldShowProviderModelId(openai, true)).toBe(true);
|
||||||
|
expect(shouldShowProviderModelId(google, true)).toBe(true);
|
||||||
|
expect(shouldShowProviderModelId(minimax, true)).toBe(true);
|
||||||
|
expect(shouldShowProviderModelId(minimaxCn, true)).toBe(true);
|
||||||
|
expect(shouldShowProviderModelId(qwen, true)).toBe(true);
|
||||||
|
|
||||||
|
expect(resolveProviderModelForSave(openai, ' ', true)).toBe('gpt-5.4');
|
||||||
|
expect(resolveProviderModelForSave(google, ' ', true)).toBe('gemini-3-pro-preview');
|
||||||
|
expect(resolveProviderModelForSave(minimax, ' ', true)).toBe('MiniMax-M2.7');
|
||||||
|
expect(resolveProviderModelForSave(minimaxCn, ' ', true)).toBe('MiniMax-M2.7');
|
||||||
|
expect(resolveProviderModelForSave(qwen, ' ', true)).toBe('coder-model');
|
||||||
|
});
|
||||||
|
|
||||||
it('saves OpenRouter and SiliconFlow model overrides by default', () => {
|
it('saves OpenRouter and SiliconFlow model overrides by default', () => {
|
||||||
const openrouter = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openrouter');
|
const openrouter = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openrouter');
|
||||||
const siliconflow = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'siliconflow');
|
const siliconflow = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'siliconflow');
|
||||||
|
|||||||
Reference in New Issue
Block a user