fix: OpenAI OAuth login and provider list cleanup (#1043)

This commit is contained in:
paisley
2026-05-19 15:51:42 +08:00
committed by GitHub
parent 0d5323360c
commit 1aa4776ea5
12 changed files with 351 additions and 45 deletions

View File

@@ -608,14 +608,10 @@ export async function syncDefaultProviderToRuntime(
logger.warn('[provider-runtime] Failed to prune invalid provider entries before switch:', err);
}
// Self-heal: pin the embedded agent runtime for any legacy provider entry
// that needs it (currently only `openai`, which would otherwise be auto-
// routed by OpenClaw to the unbundled `codex` harness when configured with
// the official api.openai.com baseUrl). Earlier ClawX builds wrote the
// provider entry without `agentRuntime`, so upgrading users would keep
// hitting `Requested agent harness "codex" is not registered.` until they
// re-saved the provider manually. Running this before every default-
// provider switch repairs the on-disk config on the next save.
// Self-heal: pin the embedded agent runtime for legacy OpenAI provider entries
// (`openai`, `openai-codex`) that would otherwise be auto-routed to the
// unbundled `codex` harness. Running this before every default-provider switch
// repairs on-disk config written by earlier ClawX builds.
try {
const pinned = await ensureOpenClawProviderAgentRuntimePins();
if (pinned.length > 0) {

View File

@@ -33,7 +33,12 @@ import {
getOpenClawProvidersConfig,
getProviderApiKeyFromOpenClaw,
} from '../../utils/openclaw-auth';
import { getAliasSourceTypes, getOpenClawProviderKeyForType } from '../../utils/provider-keys';
import {
filterActiveProviderKeysForUi,
getAliasSourceTypes,
OPENAI_CODEX_RUNTIME_PROVIDER_KEY,
resolveOpenClawProviderKey,
} from '../../utils/provider-keys';
import type { ProviderWithKeyInfo } from '../../shared/providers/types';
import { logger } from '../../utils/logger';
@@ -96,7 +101,7 @@ export class ProviderService {
// Index store accounts by their openclaw runtime key for fast lookup.
const storeByKey = new Map<string, ProviderAccount[]>();
for (const account of allStoreAccounts) {
const ock = getOpenClawProviderKeyForType(account.vendorId, account.id);
const ock = resolveOpenClawProviderKey(account);
const group = storeByKey.get(ock) ?? [];
group.push(account);
storeByKey.set(ock, group);
@@ -105,8 +110,27 @@ export class ProviderService {
const result: ProviderAccount[] = [];
const processedKeys = new Set<string>();
let hasConfiguredOpenAiApiKey = false;
if (activeProviders.has('openai')) {
for (const account of storeByKey.get('openai') ?? []) {
if (account.authMode === 'oauth_browser') {
continue;
}
const apiKey = await getApiKey(account.id);
const openClawKey = await getProviderApiKeyFromOpenClaw('openai');
if (apiKey || openClawKey) {
hasConfiguredOpenAiApiKey = true;
break;
}
}
}
const activeKeysForUi = filterActiveProviderKeysForUi(activeProviders, {
hasConfiguredOpenAiApiKey,
});
// For each active provider in openclaw.json, produce exactly ONE account.
for (const key of activeProviders) {
for (const key of activeKeysForUi) {
if (processedKeys.has(key)) continue;
processedKeys.add(key);
@@ -150,6 +174,23 @@ export class ProviderService {
}
}
if (activeProviders.has(OPENAI_CODEX_RUNTIME_PROVIDER_KEY)) {
const openaiStoreAccounts = storeByKey.get('openai') ?? [];
for (const account of openaiStoreAccounts) {
if (account.authMode !== 'api_key' && account.authMode !== undefined) {
continue;
}
const apiKey = await getApiKey(account.id);
const openClawKey = await getProviderApiKeyFromOpenClaw('openai');
if (!apiKey && !openClawKey) {
logger.info(
`[provider-sync] Removing unconfigured OpenAI API key account "${account.id}" (OAuth uses ${OPENAI_CODEX_RUNTIME_PROVIDER_KEY})`,
);
await deleteProviderAccount(account.id);
}
}
}
return result;
}
@@ -374,7 +415,7 @@ export class ProviderService {
const accounts = await this.listAccounts();
const results: Array<{ accountId: string; hasKey: boolean; keyMasked: string | null }> = [];
for (const account of accounts) {
const runtimeProviderKey = getOpenClawProviderKeyForType(account.vendorId, account.id);
const runtimeProviderKey = resolveOpenClawProviderKey(account);
const apiKey = (await getProviderApiKeyFromOpenClaw(runtimeProviderKey))
?? (await getApiKey(account.id))
?? (runtimeProviderKey !== account.id ? await getApiKey(runtimeProviderKey) : null);
@@ -396,7 +437,7 @@ export class ProviderService {
async hasAccountApiKey(accountId: string): Promise<boolean> {
const account = await this.getAccount(accountId);
const runtimeProviderKey = account
? getOpenClawProviderKeyForType(account.vendorId, account.id)
? resolveOpenClawProviderKey(account)
: accountId;
if (await getProviderApiKeyFromOpenClaw(runtimeProviderKey)) {
return true;

View File

@@ -4,7 +4,11 @@ import { logger } from './logger';
import { loginOpenAICodexOAuth, type OpenAICodexOAuthCredentials } from './openai-codex-oauth';
import { getProviderService } from '../services/providers/provider-service';
import { getSecretStore } from '../services/secrets/secret-store';
import { saveOAuthTokenToOpenClaw } from './openclaw-auth';
import {
ensureOpenClawProviderAgentRuntimePins,
saveOAuthTokenToOpenClaw,
setOpenClawDefaultModel,
} from './openclaw-auth';
// Google was removed: OpenClaw's `google-gemini-cli` OAuth integration is an
// unofficial third-party flow that requires the `gemini` CLI binary to be on
@@ -188,6 +192,26 @@ class BrowserOAuthManager extends EventEmitter {
projectId: oauthTokenSubject,
});
const modelId = normalizedExistingModel || defaultModel;
const modelRef = `${runtimeProviderId}/${modelId}`;
const fallbackModelRefs = (nextAccount.fallbackModels ?? [])
.map((fallback) => fallback.trim())
.filter(Boolean)
.map((fallback) => (
fallback.startsWith(`${runtimeProviderId}/`)
? fallback
: `${runtimeProviderId}/${fallback}`
));
try {
await setOpenClawDefaultModel(runtimeProviderId, modelRef, fallbackModelRefs);
await ensureOpenClawProviderAgentRuntimePins();
logger.info(`[BrowserOAuth] Registered ${runtimeProviderId} in openclaw.json (default model: ${modelRef})`);
} catch (err) {
logger.warn('[BrowserOAuth] Failed to register OpenAI OAuth provider in openclaw.json:', err);
throw err;
}
this.emit('oauth:success', { provider: providerType, accountId: nextAccount.id });
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('oauth:success', {

View File

@@ -115,16 +115,21 @@ function decodeJwtPayload(token: string): Record<string, unknown> | null {
function getAccountIdFromAccessToken(accessToken: string): string | null {
const payload = decodeJwtPayload(accessToken);
const authClaims = payload?.[JWT_CLAIM_PATH];
if (!authClaims || typeof authClaims !== 'object') {
return null;
if (authClaims && typeof authClaims === 'object') {
const claims = authClaims as Record<string, unknown>;
for (const key of ['chatgpt_account_id', 'account_id', 'user_id', 'sub']) {
const value = claims[key];
if (typeof value === 'string' && value.trim()) {
return value.trim();
}
}
}
const accountId = (authClaims as Record<string, unknown>).chatgpt_account_id;
if (typeof accountId !== 'string' || !accountId.trim()) {
return null;
if (typeof payload?.sub === 'string' && payload.sub.trim()) {
return payload.sub.trim();
}
return accountId;
return null;
}
async function createAuthorizationFlow(): Promise<OpenAICodexAuthorizationFlow> {
@@ -181,8 +186,9 @@ function startLocalOAuthServer(state: string): Promise<OpenAICodexLocalServer |
});
return new Promise((resolve) => {
// Bind dual-stack loopback so both `localhost` and `127.0.0.1` redirects work.
server
.listen(1455, 'localhost', () => {
.listen(1455, () => {
resolve({
close: () => server.close(),
waitForCode: async () => {
@@ -288,10 +294,7 @@ export async function loginOpenAICodexOAuth(options: {
}
const token = await exchangeAuthorizationCode(code, verifier);
const accountId = getAccountIdFromAccessToken(token.access);
if (!accountId) {
throw new Error('Failed to extract OpenAI accountId from token');
}
const accountId = getAccountIdFromAccessToken(token.access) ?? 'default';
return {
access: token.access,

View File

@@ -1161,6 +1161,21 @@ export async function setOpenClawDefaultModel(
mergeExistingModels: true,
});
console.log(`Configured models.providers.${provider} with baseUrl=${providerCfg.baseUrl}, model=${modelId}`);
} else if (provider === 'openai-codex') {
// OAuth Codex is not in the UI registry but still needs an explicit provider
// entry with a pinned embedded runtime (see OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME).
upsertOpenClawProviderEntry(config, provider, {
baseUrl: OPENAI_CODEX_OAUTH_PROVIDER_CONFIG.baseUrl,
api: OPENAI_CODEX_OAUTH_PROVIDER_CONFIG.api,
modelIds: [modelId, ...fallbackModelIds],
mergeExistingModels: true,
});
if (isOpenClawOAuthPluginProviderKey(provider)) {
ensureOAuthPluginEnabled(config, provider);
}
console.log(
`Configured models.providers.${provider} for OAuth (api=${OPENAI_CODEX_OAUTH_PROVIDER_CONFIG.api})`,
);
} else {
// Built-in provider: remove any stale models.providers entry
const models = (config.models || {}) as Record<string, unknown>;
@@ -1417,21 +1432,21 @@ export async function ensureAnthropicMessagesModelMaxTokens(): Promise<string[]>
* dispatch the chat to an externally-bundled harness plugin that may not be
* installed.
*
* Currently only the API-key OpenAI provider needs this: OpenClaw 2026.5+
* routes any `provider="openai"` entry on the official `api.openai.com`
* baseUrl through the `codex` agent harness (which expects a separate
* `codex-app-server` plugin to be installed). The bundled OpenClaw
* distribution we ship does not register a harness with id `"codex"`, so
* without this pin every API-key OpenAI chat fails with
* `Requested agent harness "codex" is not registered.` — see
* `node_modules/openclaw/dist/policy-AKMwD9k5.js` and
* `node_modules/openclaw/dist/selection-61FIEezO.js`.
*
* The OAuth flow writes to `models.providers.openai-codex` (a different key)
* and intentionally wants the codex routing, so it is not pinned here.
* OpenClaw 2026.5+ auto-routes OpenAI providers (`openai`, `openai-codex`) to the
* external `codex` agent harness, which expects a separate codex plugin install.
* The bundled OpenClaw distribution ClawX ships does not register that harness,
* so without pinning both keys chat fails with
* `Requested agent harness "codex" is not registered.`
*/
const OPENCLAW_PROVIDER_PINNED_AGENT_RUNTIME: Record<string, string> = {
openai: 'pi',
'openai-codex': 'pi',
};
/** Runtime models.providers entry for OpenAI Codex OAuth accounts. */
export const OPENAI_CODEX_OAUTH_PROVIDER_CONFIG = {
baseUrl: 'https://api.openai.com/v1',
api: 'openai-codex-responses' as const,
};
function applyPinnedAgentRuntime(

View File

@@ -3,9 +3,11 @@ const MULTI_INSTANCE_PROVIDER_TYPES = new Set(['custom', 'ollama']);
export const OPENCLAW_PROVIDER_KEY_MINIMAX = 'minimax-portal';
export const OPENCLAW_PROVIDER_KEY_MOONSHOT = 'moonshot';
export const OPENCLAW_PROVIDER_KEY_MOONSHOT_GLOBAL = 'moonshot-global';
export const OPENAI_CODEX_RUNTIME_PROVIDER_KEY = 'openai-codex';
export const OAUTH_PROVIDER_TYPES = ['minimax-portal', 'minimax-portal-cn'] as const;
export const OPENCLAW_OAUTH_PLUGIN_PROVIDER_KEYS = [
OPENCLAW_PROVIDER_KEY_MINIMAX,
OPENAI_CODEX_RUNTIME_PROVIDER_KEY,
] as const;
const OAUTH_PROVIDER_TYPE_SET = new Set<string>(OAUTH_PROVIDER_TYPES);
@@ -33,6 +35,21 @@ export function getOpenClawProviderKeyForType(type: string, providerId: string):
return PROVIDER_KEY_ALIASES[type] ?? type;
}
/**
* Resolve the OpenClaw runtime provider key for a saved account.
* Browser OAuth for OpenAI is stored under vendorId `openai` but runs as `openai-codex`.
*/
export function resolveOpenClawProviderKey(account: {
vendorId: string;
id: string;
authMode?: string;
}): string {
if (account.authMode === 'oauth_browser' && account.vendorId === 'openai') {
return OPENAI_CODEX_RUNTIME_PROVIDER_KEY;
}
return getOpenClawProviderKeyForType(account.vendorId, account.id);
}
/**
* Get all vendorId values that map to the given openclaw.json key via alias.
* e.g. getAliasSourceTypes('minimax-portal') → ['minimax-portal-cn']
@@ -43,6 +60,26 @@ export function getAliasSourceTypes(openClawKey: string): string[] {
.map(([source]) => source);
}
/**
* OpenAI Codex OAuth uses runtime key `openai-codex` while API keys use `openai`.
* When only OAuth is configured, hide the redundant bare `openai` active slot so the
* UI does not show a second "OpenAI • API key (missing)" row.
*/
export function filterActiveProviderKeysForUi(
activeKeys: Iterable<string>,
options?: { hasConfiguredOpenAiApiKey?: boolean },
): string[] {
const keys = Array.from(activeKeys);
const active = new Set(keys);
if (!active.has('openai') || !active.has(OPENAI_CODEX_RUNTIME_PROVIDER_KEY)) {
return keys;
}
if (options?.hasConfiguredOpenAiApiKey) {
return keys;
}
return keys.filter((key) => key !== 'openai');
}
export function isOAuthProviderType(type: string): boolean {
return OAUTH_PROVIDER_TYPE_SET.has(type);
}

View File

@@ -157,7 +157,6 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
requiresApiKey: true,
isOAuth: true,
supportsApiKey: true,
hideOAuthUi: true,
defaultModelId: 'gpt-5.5',
showModelId: true,
modelIdPlaceholder: 'gpt-5.5',

View File

@@ -63,6 +63,24 @@ test.describe('ClawX provider lifecycle', () => {
}
});
test('shows OpenAI OAuth and API key auth mode toggle in add-provider dialog', async ({ page }) => {
await completeSetup(page);
await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('providers-settings')).toBeVisible();
await page.getByTestId('providers-add-button').click();
await expect(page.getByTestId('add-provider-dialog')).toBeVisible();
await page.getByTestId('add-provider-type-openai').click();
await expect(page.getByRole('button', { name: 'OAuth Login' })).toBeVisible();
await expect(page.getByRole('button', { name: 'API Key' })).toBeVisible();
await page.getByRole('button', { name: 'OAuth Login' }).click();
await expect(page.getByRole('button', { name: 'Login with Browser' })).toBeVisible();
await expect(page.getByTestId('add-provider-api-key-input')).toHaveCount(0);
});
test('trims whitespace before validating and saving a custom provider key', async ({ electronApp, page }) => {
await completeSetup(page);

View File

@@ -1280,6 +1280,10 @@ describe('assertValidApiProtocol guard at write sites', () => {
await rm(testUserData, { recursive: true, force: true });
});
afterEach(() => {
vi.doUnmock('@electron/utils/provider-registry');
});
it('setOpenClawDefaultModel throws InvalidApiProtocolError and leaves openclaw.json untouched when registry api is invalid', async () => {
const initialConfig = {
agents: {
@@ -1631,7 +1635,7 @@ describe('openai agentRuntime pin', () => {
expect(openai.baseUrl).toBe('https://api.openai.com/v1');
});
it('does not pin agentRuntime for the OAuth openai-codex provider entry', async () => {
it('pins agentRuntime to the embedded "pi" runtime for the OAuth openai-codex provider entry', async () => {
await writeOpenClawJson({
models: { providers: {} },
});
@@ -1648,7 +1652,8 @@ describe('openai agentRuntime pin', () => {
const codex = providers['openai-codex'] as Record<string, unknown>;
expect(codex).toBeDefined();
expect(codex.agentRuntime).toBeUndefined();
expect(codex.agentRuntime).toEqual({ id: 'pi' });
expect(codex.api).toBe('openai-codex-responses');
});
it('preserves a user-provided agentRuntime override on the openai entry', async () => {
@@ -1682,6 +1687,34 @@ describe('openai agentRuntime pin', () => {
});
});
describe('setOpenClawDefaultModel for openai-codex OAuth', () => {
beforeEach(async () => {
vi.doUnmock('@electron/utils/provider-registry');
vi.resetModules();
vi.restoreAllMocks();
await rm(testHome, { recursive: true, force: true });
await rm(testUserData, { recursive: true, force: true });
});
it('writes models.providers.openai-codex with a pinned pi runtime', async () => {
await writeOpenClawJson({
models: { providers: {} },
});
const { setOpenClawDefaultModel } = await import('@electron/utils/openclaw-auth');
await setOpenClawDefaultModel('openai-codex', 'openai-codex/gpt-5.5');
const result = await readOpenClawJson();
const providers = (result.models as Record<string, unknown>).providers as Record<string, unknown>;
const codex = providers['openai-codex'] as Record<string, unknown>;
const defaults = ((result.agents as Record<string, unknown>).defaults as Record<string, unknown>).model as Record<string, unknown>;
expect(defaults.primary).toBe('openai-codex/gpt-5.5');
expect(codex.agentRuntime).toEqual({ id: 'pi' });
expect(codex.api).toBe('openai-codex-responses');
});
});
describe('ensureOpenClawProviderAgentRuntimePins', () => {
beforeEach(async () => {
vi.resetModules();
@@ -1715,6 +1748,29 @@ describe('ensureOpenClawProviderAgentRuntimePins', () => {
expect(openai.api).toBe('openai-responses');
});
it('pins agentRuntime:{id:"pi"} on legacy openai-codex OAuth entries that lack it', async () => {
await writeOpenClawJson({
models: {
providers: {
'openai-codex': {
baseUrl: 'https://api.openai.com/v1',
api: 'openai-codex-responses',
models: [{ id: 'gpt-5.5', name: 'gpt-5.5' }],
},
},
},
});
const { ensureOpenClawProviderAgentRuntimePins } = await import('@electron/utils/openclaw-auth');
const pinned = await ensureOpenClawProviderAgentRuntimePins();
expect(pinned).toEqual(['openai-codex']);
const result = await readOpenClawJson();
const providers = (result.models as Record<string, unknown>).providers as Record<string, unknown>;
const codex = providers['openai-codex'] as Record<string, unknown>;
expect(codex.agentRuntime).toEqual({ id: 'pi' });
});
it('leaves entries untouched when the openai entry already has any agentRuntime.id', async () => {
const initial = {
models: {

View File

@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';
import {
OPENAI_CODEX_RUNTIME_PROVIDER_KEY,
filterActiveProviderKeysForUi,
getOpenClawProviderKeyForType,
isOpenClawOAuthPluginProviderKey,
resolveOpenClawProviderKey,
} from '@electron/utils/provider-keys';
describe('provider-keys', () => {
it('maps OpenAI browser OAuth accounts to the openai-codex runtime key', () => {
expect(resolveOpenClawProviderKey({
vendorId: 'openai',
id: 'openai-personal',
authMode: 'oauth_browser',
})).toBe(OPENAI_CODEX_RUNTIME_PROVIDER_KEY);
expect(resolveOpenClawProviderKey({
vendorId: 'openai',
id: 'openai-personal',
authMode: 'api_key',
})).toBe('openai');
});
it('keeps custom multi-instance hashing behavior', () => {
expect(getOpenClawProviderKeyForType('custom', 'my-local')).toBe('custom-mylocal');
});
it('treats openai-codex as an OAuth plugin provider key', () => {
expect(isOpenClawOAuthPluginProviderKey('openai-codex')).toBe(true);
});
it('drops bare openai from the UI list when only openai-codex OAuth is active', () => {
expect(filterActiveProviderKeysForUi(['openai', 'openai-codex', 'anthropic'])).toEqual([
'openai-codex',
'anthropic',
]);
});
it('keeps openai in the UI list when an API key is configured alongside Codex OAuth', () => {
expect(filterActiveProviderKeysForUi(['openai', 'openai-codex'], {
hasConfiguredOpenAiApiKey: true,
})).toEqual(['openai', 'openai-codex']);
});
});

View File

@@ -38,10 +38,20 @@ vi.mock('@electron/utils/openclaw-auth', () => ({
getProviderApiKeyFromOpenClaw: mocks.getProviderApiKeyFromOpenClaw,
}));
vi.mock('@electron/utils/provider-keys', () => ({
getOpenClawProviderKeyForType: mocks.getOpenClawProviderKeyForType,
getAliasSourceTypes: mocks.getAliasSourceTypes,
}));
vi.mock('@electron/utils/provider-keys', async (importOriginal) => {
const actual = await importOriginal<typeof import('@electron/utils/provider-keys')>();
return {
...actual,
getOpenClawProviderKeyForType: mocks.getOpenClawProviderKeyForType,
resolveOpenClawProviderKey: (account: { vendorId: string; id: string; authMode?: string }) => {
if (account.authMode === 'oauth_browser' && account.vendorId === 'openai') {
return 'openai-codex';
}
return mocks.getOpenClawProviderKeyForType(account.vendorId, account.id);
},
getAliasSourceTypes: mocks.getAliasSourceTypes,
};
});
vi.mock('@electron/utils/secure-storage', () => ({
deleteApiKey: vi.fn(),
@@ -174,6 +184,62 @@ describe('ProviderService.listAccounts (openclaw.json as sole source of truth)',
expect(result[0].label).toBe('My Moonshot');
});
it('hides the bare openai slot when only openai-codex OAuth is configured', async () => {
mocks.listProviderAccounts.mockResolvedValue([
makeAccount({
id: 'openai-oauth-1',
vendorId: 'openai' as ProviderAccount['vendorId'],
authMode: 'oauth_browser',
label: 'OpenAI Codex',
}),
makeAccount({
id: 'openai',
vendorId: 'openai' as ProviderAccount['vendorId'],
authMode: 'api_key',
label: 'OpenAI',
}),
]);
mocks.getApiKey.mockResolvedValue(null);
mocks.getProviderApiKeyFromOpenClaw.mockResolvedValue(null);
mocks.getActiveOpenClawProviders.mockResolvedValue(new Set(['openai', 'openai-codex']));
mocks.getOpenClawProvidersConfig.mockResolvedValue({
providers: {
openai: { baseUrl: 'https://api.openai.com/v1', api: 'openai-responses' },
'openai-codex': { baseUrl: 'https://api.openai.com/v1', api: 'openai-codex-responses' },
},
defaultModel: 'openai-codex/gpt-5.5',
});
const result = await service.listAccounts();
expect(result).toHaveLength(1);
expect(result[0].id).toBe('openai-oauth-1');
expect(mocks.deleteProviderAccount).toHaveBeenCalledWith('openai');
});
it('matches OpenAI browser OAuth accounts to the openai-codex runtime key', async () => {
mocks.listProviderAccounts.mockResolvedValue([
makeAccount({
id: 'openai-oauth-1',
vendorId: 'openai' as ProviderAccount['vendorId'],
authMode: 'oauth_browser',
label: 'OpenAI Codex',
}),
]);
mocks.getActiveOpenClawProviders.mockResolvedValue(new Set(['openai-codex']));
mocks.getOpenClawProvidersConfig.mockResolvedValue({
providers: {},
defaultModel: 'openai-codex/gpt-5.5',
});
const result = await service.listAccounts();
expect(mocks.saveProviderAccount).not.toHaveBeenCalled();
expect(result).toHaveLength(1);
expect(result[0].id).toBe('openai-oauth-1');
expect(result[0].authMode).toBe('oauth_browser');
});
it('matches UUID-based store account to openclaw key via getOpenClawProviderKeyForType', async () => {
mocks.listProviderAccounts.mockResolvedValue([
makeAccount({ id: 'openrouter-uuid-1234', vendorId: 'openrouter' as ProviderAccount['vendorId'] }),

View File

@@ -150,7 +150,13 @@ describe('provider metadata', () => {
const minimax = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'minimax-portal');
const minimaxCn = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'minimax-portal-cn');
expect(openai).toMatchObject({ showModelId: true, defaultModelId: 'gpt-5.5' });
expect(openai).toMatchObject({
showModelId: true,
defaultModelId: 'gpt-5.5',
isOAuth: true,
supportsApiKey: true,
});
expect(openai?.hideOAuthUi).toBeUndefined();
expect(google).toMatchObject({ showModelId: true, defaultModelId: 'gemini-3.1-pro-preview' });
expect(minimax).toMatchObject({ showModelId: true, defaultModelId: 'MiniMax-M2.7' });
expect(minimaxCn).toMatchObject({ showModelId: true, defaultModelId: 'MiniMax-M2.7' });