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 {
|
||||
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(
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user