feat: add new provider for minimax and qwen portals (#203)
Co-authored-by: Haze <709547807@qq.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
5d548da2e6
commit
f70d5b0c28
@@ -59,12 +59,14 @@ import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
||||
* @param providerId - Unique provider ID from secure-storage (UUID-like)
|
||||
* @returns A string like 'custom-a1b2c3d4' or 'openrouter'
|
||||
*/
|
||||
function getOpenClawProviderKey(type: string, providerId: string): string {
|
||||
export function getOpenClawProviderKey(type: string, providerId: string): string {
|
||||
if (type === 'custom' || type === 'ollama') {
|
||||
// Use the first 8 chars of the providerId as a stable short suffix
|
||||
const suffix = providerId.replace(/-/g, '').slice(0, 8);
|
||||
return `${type}-${suffix}`;
|
||||
}
|
||||
if (type === 'minimax-portal-cn') {
|
||||
return 'minimax-portal';
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -834,6 +836,14 @@ function registerDeviceOAuthHandlers(mainWindow: BrowserWindow): void {
|
||||
* Provider-related IPC handlers
|
||||
*/
|
||||
function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
// Listen for OAuth success to automatically restart the Gateway with new tokens/configs
|
||||
deviceOAuthManager.on('oauth:success', (providerType) => {
|
||||
logger.info(`[IPC] Restarting Gateway after ${providerType} OAuth success...`);
|
||||
void gatewayManager.restart().catch(err => {
|
||||
logger.error('Failed to restart Gateway after OAuth success:', err);
|
||||
});
|
||||
});
|
||||
|
||||
// Get all providers with key info
|
||||
ipcMain.handle('provider:list', async () => {
|
||||
return await getAllProvidersWithKeyInfo();
|
||||
@@ -923,6 +933,12 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
try {
|
||||
const ock = getOpenClawProviderKey(existing.type, providerId);
|
||||
removeProviderFromOpenClaw(ock);
|
||||
|
||||
// Restart Gateway so it no longer loads the deleted provider's plugin/config
|
||||
logger.info(`Restarting Gateway after deleting provider "${ock}"`);
|
||||
void gatewayManager.restart().catch((err) => {
|
||||
logger.warn('Gateway restart after provider delete failed:', err);
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('Failed to completely remove provider from OpenClaw:', err);
|
||||
}
|
||||
@@ -1114,9 +1130,9 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
const ock = getOpenClawProviderKey(provider.type, providerId);
|
||||
const providerKey = await getApiKey(providerId);
|
||||
|
||||
// OAuth providers (qwen-portal, minimax-portal) might use OAuth OR a direct API key.
|
||||
// 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.
|
||||
const OAUTH_PROVIDER_TYPES = ['qwen-portal', 'minimax-portal'];
|
||||
const OAUTH_PROVIDER_TYPES = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
|
||||
const isOAuthProvider = OAUTH_PROVIDER_TYPES.includes(provider.type) && !providerKey;
|
||||
|
||||
if (!isOAuthProvider) {
|
||||
@@ -1141,23 +1157,30 @@ function registerProviderHandlers(gatewayManager: GatewayManager): void {
|
||||
saveProviderKeyToOpenClaw(ock, providerKey);
|
||||
}
|
||||
} else {
|
||||
// OAuth providers (minimax-portal, qwen-portal)
|
||||
// OAuth providers (minimax-portal, minimax-portal-cn, qwen-portal)
|
||||
const defaultBaseUrl = provider.type === 'minimax-portal'
|
||||
? 'https://api.minimax.io/anthropic'
|
||||
: 'https://portal.qwen.ai/v1';
|
||||
const api: 'anthropic-messages' | 'openai-completions' = provider.type === 'minimax-portal'
|
||||
? 'anthropic-messages'
|
||||
: 'openai-completions';
|
||||
: (provider.type === 'minimax-portal-cn' ? 'https://api.minimaxi.com/anthropic' : 'https://portal.qwen.ai/v1');
|
||||
const api: 'anthropic-messages' | 'openai-completions' =
|
||||
(provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn')
|
||||
? 'anthropic-messages'
|
||||
: 'openai-completions';
|
||||
|
||||
let baseUrl = provider.baseUrl || defaultBaseUrl;
|
||||
if (provider.type === 'minimax-portal' && baseUrl && !baseUrl.endsWith('/anthropic')) {
|
||||
if ((provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn') && baseUrl && !baseUrl.endsWith('/anthropic')) {
|
||||
baseUrl = baseUrl.replace(/\/$/, '') + '/anthropic';
|
||||
}
|
||||
|
||||
setOpenClawDefaultModelWithOverride(provider.type, undefined, {
|
||||
// To ensure the OpenClaw Gateway's internal token refresher works,
|
||||
// we must save the CN provider under the "minimax-portal" key in openclaw.json
|
||||
const targetProviderKey = (provider.type === 'minimax-portal' || provider.type === 'minimax-portal-cn')
|
||||
? 'minimax-portal'
|
||||
: provider.type;
|
||||
|
||||
setOpenClawDefaultModelWithOverride(targetProviderKey, undefined, {
|
||||
baseUrl,
|
||||
api,
|
||||
apiKeyEnv: provider.type === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
||||
apiKeyEnv: targetProviderKey === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
||||
});
|
||||
|
||||
logger.info(`Configured openclaw.json for OAuth provider "${provider.type}"`);
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from '../../node_modules/openclaw/extensions/qwen-portal-auth/oauth';
|
||||
import { saveOAuthTokenToOpenClaw, setOpenClawDefaultModelWithOverride } from './openclaw-auth';
|
||||
|
||||
export type OAuthProviderType = 'minimax-portal' | 'qwen-portal';
|
||||
export type OAuthProviderType = 'minimax-portal' | 'minimax-portal-cn' | 'qwen-portal';
|
||||
export type { MiniMaxRegion };
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@@ -55,15 +55,17 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
}
|
||||
|
||||
this.active = true;
|
||||
this.emit('oauth:start', { provider: provider });
|
||||
this.activeProvider = provider;
|
||||
|
||||
try {
|
||||
if (provider === 'minimax-portal') {
|
||||
await this.runMiniMaxFlow(region);
|
||||
if (provider === 'minimax-portal' || provider === 'minimax-portal-cn') {
|
||||
const actualRegion = provider === 'minimax-portal-cn' ? 'cn' : (region || 'global');
|
||||
await this.runMiniMaxFlow(actualRegion, provider);
|
||||
} else if (provider === 'qwen-portal') {
|
||||
await this.runQwenFlow();
|
||||
} else {
|
||||
throw new Error(`Unsupported OAuth provider: ${provider}`);
|
||||
throw new Error(`Unsupported OAuth provider type: ${provider}`);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -89,7 +91,7 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
// MiniMax flow
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
private async runMiniMaxFlow(region: MiniMaxRegion): Promise<void> {
|
||||
private async runMiniMaxFlow(region?: MiniMaxRegion, providerType: OAuthProviderType = 'minimax-portal'): Promise<void> {
|
||||
if (!isOpenClawPresent()) {
|
||||
throw new Error('OpenClaw package not found');
|
||||
}
|
||||
@@ -123,7 +125,7 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
|
||||
if (!this.active) return;
|
||||
|
||||
await this.onSuccess('minimax-portal', {
|
||||
await this.onSuccess(providerType, {
|
||||
access: token.access,
|
||||
refresh: token.refresh,
|
||||
expires: token.expires,
|
||||
@@ -131,6 +133,7 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
resourceUrl: token.resourceUrl,
|
||||
// MiniMax uses Anthropic Messages API format
|
||||
api: 'anthropic-messages',
|
||||
region,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,15 +192,19 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
expires: number;
|
||||
resourceUrl?: string;
|
||||
api: 'anthropic-messages' | 'openai-completions';
|
||||
region?: MiniMaxRegion;
|
||||
}) {
|
||||
this.active = false;
|
||||
this.activeProvider = null;
|
||||
logger.info(`[DeviceOAuth] Successfully completed OAuth for ${providerType}`);
|
||||
|
||||
// 1. Write OAuth token to OpenClaw's auth-profiles.json in native OAuth format
|
||||
// (matches what `openclaw models auth login` → upsertAuthProfile writes)
|
||||
// 1. Write OAuth token to OpenClaw's auth-profiles.json in native OAuth format.
|
||||
// (matches what `openclaw models auth login` → upsertAuthProfile writes).
|
||||
// We save both MiniMax providers to the generic "minimax-portal" profile
|
||||
// so OpenClaw's gateway auto-refresher knows how to find it.
|
||||
try {
|
||||
saveOAuthTokenToOpenClaw(providerType, {
|
||||
const tokenProviderId = providerType.startsWith('minimax-portal') ? 'minimax-portal' : providerType;
|
||||
saveOAuthTokenToOpenClaw(tokenProviderId, {
|
||||
access: token.access,
|
||||
refresh: token.refresh,
|
||||
expires: token.expires,
|
||||
@@ -213,18 +220,19 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
// Note: MiniMax Anthropic-compatible API requires the /anthropic suffix.
|
||||
const defaultBaseUrl = providerType === 'minimax-portal'
|
||||
? 'https://api.minimax.io/anthropic'
|
||||
: 'https://portal.qwen.ai/v1';
|
||||
: (providerType === 'minimax-portal-cn' ? 'https://api.minimaxi.com/anthropic' : 'https://portal.qwen.ai/v1');
|
||||
|
||||
let baseUrl = token.resourceUrl || defaultBaseUrl;
|
||||
|
||||
// If MiniMax returned a resourceUrl (e.g. https://api.minimax.io) but no /anthropic suffix,
|
||||
// we must append it because we use the 'anthropic-messages' API mode
|
||||
if (providerType === 'minimax-portal' && baseUrl && !baseUrl.endsWith('/anthropic')) {
|
||||
if (providerType.startsWith('minimax-portal') && baseUrl && !baseUrl.endsWith('/anthropic')) {
|
||||
baseUrl = baseUrl.replace(/\/$/, '') + '/anthropic';
|
||||
}
|
||||
|
||||
try {
|
||||
setOpenClawDefaultModelWithOverride(providerType, undefined, {
|
||||
const tokenProviderId = providerType.startsWith('minimax-portal') ? 'minimax-portal' : providerType;
|
||||
setOpenClawDefaultModelWithOverride(tokenProviderId, undefined, {
|
||||
baseUrl,
|
||||
api: token.api,
|
||||
// OAuth placeholder — tells Gateway to resolve credentials
|
||||
@@ -232,7 +240,7 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
// This matches what the OpenClaw plugin's configPatch writes:
|
||||
// minimax-portal → apiKey: 'minimax-oauth'
|
||||
// qwen-portal → apiKey: 'qwen-oauth'
|
||||
apiKeyEnv: providerType === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
||||
apiKeyEnv: tokenProviderId === 'minimax-portal' ? 'minimax-oauth' : 'qwen-oauth',
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(`[DeviceOAuth] Failed to configure openclaw models:`, err);
|
||||
@@ -240,19 +248,28 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
|
||||
// 3. Save provider record in ClawX's own store so UI shows it as configured
|
||||
const existing = await getProvider(providerType);
|
||||
const nameMap: Record<OAuthProviderType, string> = {
|
||||
'minimax-portal': 'MiniMax (Global)',
|
||||
'minimax-portal-cn': 'MiniMax (CN)',
|
||||
'qwen-portal': 'Qwen',
|
||||
};
|
||||
const providerConfig: ProviderConfig = {
|
||||
id: providerType,
|
||||
name: providerType === 'minimax-portal' ? 'MiniMax' : 'Qwen',
|
||||
name: nameMap[providerType as OAuthProviderType] || providerType,
|
||||
type: providerType,
|
||||
enabled: existing?.enabled ?? true,
|
||||
baseUrl: existing?.baseUrl,
|
||||
baseUrl, // Save the dynamically resolved URL (Global vs CN)
|
||||
|
||||
model: existing?.model || getProviderDefaultModel(providerType),
|
||||
createdAt: existing?.createdAt || new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await saveProvider(providerConfig);
|
||||
|
||||
// 4. Emit success to frontend
|
||||
// 4. Emit success internally so the main process can restart the Gateway
|
||||
this.emit('oauth:success', providerType);
|
||||
|
||||
// 5. Emit success to frontend
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.webContents.send('oauth:success', { provider: providerType, success: true });
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ export function saveProviderKeyToOpenClaw(
|
||||
// managed by OpenClaw plugins via `openclaw models auth login`.
|
||||
// Skip only if there's no explicit API key — meaning the user is using OAuth.
|
||||
// If the user provided an actual API key, write it normally.
|
||||
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal'];
|
||||
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
|
||||
if (OAUTH_PROVIDERS.includes(provider) && !apiKey) {
|
||||
console.log(`Skipping auth-profiles write for OAuth provider "${provider}" (no API key provided, using OAuth)`);
|
||||
return;
|
||||
@@ -227,7 +227,7 @@ export function removeProviderKeyFromOpenClaw(
|
||||
): void {
|
||||
// OAuth providers have their credentials managed by OpenClaw plugins.
|
||||
// Do NOT delete their auth-profiles entries.
|
||||
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal'];
|
||||
const OAUTH_PROVIDERS = ['qwen-portal', 'minimax-portal', 'minimax-portal-cn'];
|
||||
if (OAUTH_PROVIDERS.includes(provider)) {
|
||||
console.log(`Skipping auth-profiles removal for OAuth provider "${provider}" (managed by OpenClaw plugin)`);
|
||||
return;
|
||||
|
||||
@@ -13,6 +13,7 @@ export const BUILTIN_PROVIDER_TYPES = [
|
||||
'moonshot',
|
||||
'siliconflow',
|
||||
'minimax-portal',
|
||||
'minimax-portal-cn',
|
||||
'qwen-portal',
|
||||
'ollama',
|
||||
] as const;
|
||||
@@ -110,6 +111,15 @@ const REGISTRY: Record<string, ProviderBackendMeta> = {
|
||||
apiKeyEnv: 'MINIMAX_API_KEY',
|
||||
},
|
||||
},
|
||||
'minimax-portal-cn': {
|
||||
envVar: 'MINIMAX_CN_API_KEY',
|
||||
defaultModel: 'minimax-portal/MiniMax-M2.1',
|
||||
providerConfig: {
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
api: 'anthropic-messages',
|
||||
apiKeyEnv: 'MINIMAX_CN_API_KEY',
|
||||
},
|
||||
},
|
||||
'qwen-portal': {
|
||||
envVar: 'QWEN_API_KEY',
|
||||
defaultModel: 'qwen-portal/coder-model',
|
||||
|
||||
@@ -234,7 +234,7 @@ export async function getAllProvidersWithKeyInfo(): Promise<
|
||||
// This must match getOpenClawProviderKey() in ipc-handlers.ts exactly.
|
||||
const openClawKey = (provider.type === 'custom' || provider.type === 'ollama')
|
||||
? `${provider.type}-${provider.id.replace(/-/g, '').slice(0, 8)}`
|
||||
: provider.type;
|
||||
: provider.type === 'minimax-portal-cn' ? 'minimax-portal' : provider.type;
|
||||
if (!isBuiltin && !activeOpenClawProviders.has(provider.type) && !activeOpenClawProviders.has(provider.id) && !activeOpenClawProviders.has(openClawKey)) {
|
||||
console.log(`[Sync] Provider ${provider.id} (${provider.type}) missing from OpenClaw, dropping from ClawX UI`);
|
||||
await deleteProvider(provider.id);
|
||||
|
||||
@@ -167,6 +167,7 @@ function App() {
|
||||
position="bottom-right"
|
||||
richColors
|
||||
closeButton
|
||||
style={{ zIndex: 99999 }}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -17,6 +17,7 @@ export const providerIcons: Record<string, string> = {
|
||||
moonshot,
|
||||
siliconflow,
|
||||
'minimax-portal': minimaxPortal,
|
||||
'minimax-portal-cn': minimaxPortal,
|
||||
'qwen-portal': qwenPortal,
|
||||
ollama,
|
||||
custom,
|
||||
|
||||
@@ -487,20 +487,19 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
|
||||
setOauthFlowing(false);
|
||||
setOauthData(null);
|
||||
setValidationError(null);
|
||||
const { selectedType: type, typeInfo: info, onAdd: add, onClose: close, t: translate } = latestRef.current;
|
||||
// Save the provider to the store so the list refreshes automatically
|
||||
if (type && add) {
|
||||
try {
|
||||
await add(
|
||||
type,
|
||||
info?.name || type,
|
||||
'', // OAuth providers don't use a plain API key
|
||||
{ model: info?.defaultModelId }
|
||||
);
|
||||
} catch {
|
||||
// provider may already exist; ignore duplicate errors
|
||||
}
|
||||
|
||||
const { onClose: close, t: translate } = latestRef.current;
|
||||
|
||||
// device-oauth.ts already saved the provider config to the backend,
|
||||
// including the dynamically resolved baseUrl for the region (e.g. CN vs Global).
|
||||
// If we call add() here with undefined baseUrl, it will overwrite and erase it!
|
||||
// So we just fetch the latest list from the backend to update the UI.
|
||||
try {
|
||||
await useProviderStore.getState().fetchProviders();
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh providers after OAuth:', err);
|
||||
}
|
||||
|
||||
close();
|
||||
toast.success(translate('aiProviders.toast.added'));
|
||||
};
|
||||
@@ -525,12 +524,22 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
|
||||
|
||||
const handleStartOAuth = async () => {
|
||||
if (!selectedType) return;
|
||||
|
||||
if (selectedType === 'minimax-portal' && existingTypes.has('minimax-portal-cn')) {
|
||||
toast.error(t('aiProviders.toast.minimaxConflict'));
|
||||
return;
|
||||
}
|
||||
if (selectedType === 'minimax-portal-cn' && existingTypes.has('minimax-portal')) {
|
||||
toast.error(t('aiProviders.toast.minimaxConflict'));
|
||||
return;
|
||||
}
|
||||
|
||||
setOauthFlowing(true);
|
||||
setOauthData(null);
|
||||
setOauthError(null);
|
||||
|
||||
try {
|
||||
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedType, 'global');
|
||||
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedType);
|
||||
} catch (e) {
|
||||
setOauthError(String(e));
|
||||
setOauthFlowing(false);
|
||||
@@ -552,6 +561,15 @@ function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: Add
|
||||
const handleAdd = async () => {
|
||||
if (!selectedType) return;
|
||||
|
||||
if (selectedType === 'minimax-portal' && existingTypes.has('minimax-portal-cn')) {
|
||||
toast.error(t('aiProviders.toast.minimaxConflict'));
|
||||
return;
|
||||
}
|
||||
if (selectedType === 'minimax-portal-cn' && existingTypes.has('minimax-portal')) {
|
||||
toast.error(t('aiProviders.toast.minimaxConflict'));
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setValidationError(null);
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
"updated": "Provider updated",
|
||||
"failedUpdate": "Failed to update provider",
|
||||
"invalidKey": "Invalid API key",
|
||||
"modelRequired": "Model ID is required"
|
||||
"modelRequired": "Model ID is required",
|
||||
"minimaxConflict": "Cannot add both MiniMax (Global) and MiniMax (CN) providers."
|
||||
},
|
||||
"oauth": {
|
||||
"loginMode": "OAuth Login",
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
"updated": "提供商已更新",
|
||||
"failedUpdate": "更新提供商失败",
|
||||
"invalidKey": "无效的 API 密钥",
|
||||
"modelRequired": "需要模型 ID"
|
||||
"modelRequired": "需要模型 ID",
|
||||
"minimaxConflict": "不能同时添加 MiniMax 国际站和国内站的服务商。"
|
||||
},
|
||||
"oauth": {
|
||||
"loginMode": "OAuth 登录",
|
||||
|
||||
@@ -13,6 +13,7 @@ export const PROVIDER_TYPES = [
|
||||
'moonshot',
|
||||
'siliconflow',
|
||||
'minimax-portal',
|
||||
'minimax-portal-cn',
|
||||
'qwen-portal',
|
||||
'ollama',
|
||||
'custom',
|
||||
@@ -69,7 +70,8 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
|
||||
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true },
|
||||
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5' },
|
||||
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', defaultModelId: 'Pro/moonshotai/Kimi-K2.5' },
|
||||
{ id: 'minimax-portal', name: 'MiniMax', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.1' },
|
||||
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.1' },
|
||||
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.1' },
|
||||
{ id: 'qwen-portal', name: 'Qwen', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'coder-model' },
|
||||
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },
|
||||
{ id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...', requiresApiKey: true, showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'your-provider/model-id' },
|
||||
|
||||
@@ -773,14 +773,28 @@ function ProviderContent({
|
||||
|
||||
const handleStartOAuth = async () => {
|
||||
if (!selectedProvider) return;
|
||||
|
||||
try {
|
||||
const list = await window.electron.ipcRenderer.invoke('provider:list') as Array<{ type: string }>;
|
||||
const existingTypes = new Set(list.map(l => l.type));
|
||||
if (selectedProvider === 'minimax-portal' && existingTypes.has('minimax-portal-cn')) {
|
||||
toast.error(t('settings:aiProviders.toast.minimaxConflict'));
|
||||
return;
|
||||
}
|
||||
if (selectedProvider === 'minimax-portal-cn' && existingTypes.has('minimax-portal')) {
|
||||
toast.error(t('settings:aiProviders.toast.minimaxConflict'));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore check failure
|
||||
}
|
||||
|
||||
setOauthFlowing(true);
|
||||
setOauthData(null);
|
||||
setOauthError(null);
|
||||
|
||||
// Default to global region for MiniMax in setup
|
||||
const region = 'global';
|
||||
try {
|
||||
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedProvider, region);
|
||||
await window.electron.ipcRenderer.invoke('provider:requestOAuth', selectedProvider);
|
||||
} catch (e) {
|
||||
setOauthError(String(e));
|
||||
setOauthFlowing(false);
|
||||
@@ -905,6 +919,21 @@ function ProviderContent({
|
||||
const handleValidateAndSave = async () => {
|
||||
if (!selectedProvider) return;
|
||||
|
||||
try {
|
||||
const list = await window.electron.ipcRenderer.invoke('provider:list') as Array<{ type: string }>;
|
||||
const existingTypes = new Set(list.map(l => l.type));
|
||||
if (selectedProvider === 'minimax-portal' && existingTypes.has('minimax-portal-cn')) {
|
||||
toast.error(t('settings:aiProviders.toast.minimaxConflict'));
|
||||
return;
|
||||
}
|
||||
if (selectedProvider === 'minimax-portal-cn' && existingTypes.has('minimax-portal')) {
|
||||
toast.error(t('settings:aiProviders.toast.minimaxConflict'));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore check failure
|
||||
}
|
||||
|
||||
setValidating(true);
|
||||
setKeyValid(null);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user