fix: clean up deleted provider state correctly (#696)

This commit is contained in:
Lingxuan Zuo
2026-03-27 23:32:56 +08:00
committed by GitHub
Unverified
parent 07f3c310b5
commit 9b56d80d22
8 changed files with 510 additions and 32 deletions

View File

@@ -6,6 +6,7 @@ import { getAllProviders, getApiKey, getDefaultProvider, getProvider } from '../
import { getProviderConfig, getProviderDefaultModel } from '../../utils/provider-registry';
import {
removeProviderFromOpenClaw,
removeProviderKeyFromOpenClaw,
saveOAuthTokenToOpenClaw,
saveProviderKeyToOpenClaw,
setOpenClawDefaultModel,
@@ -20,7 +21,7 @@ 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.3-codex`;
const OPENAI_OAUTH_DEFAULT_MODEL_REF = `${OPENAI_OAUTH_RUNTIME_PROVIDER}/gpt-5.4`;
type RuntimeProviderSyncContext = {
runtimeProviderKey: string;
@@ -347,6 +348,24 @@ async function syncProviderToRuntime(
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('/');
@@ -538,7 +557,7 @@ export async function syncDeletedProviderToRuntime(
}
const ock = runtimeProviderKey ?? await resolveRuntimeProviderKey({ ...provider, id: providerId });
await removeProviderFromOpenClaw(ock);
await removeDeletedProviderFromOpenClaw(provider, providerId, ock);
scheduleGatewayRefresh(
gatewayManager,
@@ -557,7 +576,7 @@ export async function syncDeletedProviderApiKeyToRuntime(
}
const ock = runtimeProviderKey ?? await resolveRuntimeProviderKey({ ...provider, id: providerId });
await removeProviderFromOpenClaw(ock);
await removeProviderKeyFromOpenClaw(ock);
}
export async function syncDefaultProviderToRuntime(

View File

@@ -29,9 +29,12 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
requiresApiKey: true,
category: 'official',
envVar: 'OPENAI_API_KEY',
defaultModelId: 'gpt-5.2',
defaultModelId: 'gpt-5.4',
isOAuth: true,
supportsApiKey: true,
showModelId: true,
showModelIdInDevModeOnly: true,
modelIdPlaceholder: 'gpt-5.4',
supportedAuthModes: ['api_key', 'oauth_browser'],
defaultAuthMode: 'api_key',
supportsMultipleAccounts: true,
@@ -50,9 +53,12 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
requiresApiKey: true,
category: 'official',
envVar: 'GEMINI_API_KEY',
defaultModelId: 'gemini-3.1-pro-preview',
defaultModelId: 'gemini-3-pro-preview',
isOAuth: true,
supportsApiKey: true,
showModelId: true,
showModelIdInDevModeOnly: true,
modelIdPlaceholder: 'gemini-3-pro-preview',
supportedAuthModes: ['api_key', 'oauth_browser'],
defaultAuthMode: 'api_key',
supportsMultipleAccounts: true,
@@ -171,6 +177,9 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
isOAuth: true,
supportsApiKey: true,
defaultModelId: 'MiniMax-M2.7',
showModelId: true,
showModelIdInDevModeOnly: true,
modelIdPlaceholder: 'MiniMax-M2.7',
apiKeyUrl: 'https://platform.minimax.io',
category: 'official',
envVar: 'MINIMAX_API_KEY',
@@ -193,6 +202,9 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
isOAuth: true,
supportsApiKey: true,
defaultModelId: 'MiniMax-M2.7',
showModelId: true,
showModelIdInDevModeOnly: true,
modelIdPlaceholder: 'MiniMax-M2.7',
apiKeyUrl: 'https://platform.minimaxi.com/',
category: 'official',
envVar: 'MINIMAX_CN_API_KEY',
@@ -214,6 +226,9 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
requiresApiKey: false,
isOAuth: true,
defaultModelId: 'coder-model',
showModelId: true,
showModelIdInDevModeOnly: true,
modelIdPlaceholder: 'coder-model',
category: 'official',
envVar: 'QWEN_API_KEY',
supportedAuthModes: ['oauth_device'],

View File

@@ -12,7 +12,7 @@ export type BrowserOAuthProviderType = 'google' | 'openai';
const GOOGLE_RUNTIME_PROVIDER_ID = 'google-gemini-cli';
const GOOGLE_OAUTH_DEFAULT_MODEL = 'gemini-3-pro-preview';
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 {
private activeProvider: BrowserOAuthProviderType | null = null;

View File

@@ -93,6 +93,82 @@ interface AuthProfilesStore {
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 ────────────────────────────────────────────
function getAuthProfilesPath(agentId = 'main'): string {
@@ -346,26 +422,14 @@ export async function removeProviderKeyFromOpenClaw(
provider: string,
agentId?: string
): 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();
if (agentIds.length === 0) agentIds.push('main');
for (const id of agentIds) {
const store = await readAuthProfiles(id);
const profileId = `${provider}:default`;
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 (removeProfileFromStore(store, `${provider}:default`, 'api_key')) {
await writeAuthProfiles(store, id);
}
if (store.lastGood?.[provider] === profileId) delete store.lastGood[provider];
await writeAuthProfiles(store, id);
}
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');
for (const id of agentIds) {
const store = await readAuthProfiles(id);
const profileId = `${provider}:default`;
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];
if (removeProfilesForProvider(store, provider)) {
await writeAuthProfiles(store, id);
}
}
@@ -435,6 +492,25 @@ export async function removeProviderFromOpenClaw(provider: string): Promise<void
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.
// Model refs use the format "providerType/modelId", e.g. "openai/gpt-4".
// Leaving stale refs causes the Gateway to report "Unknown model" errors.