feat: add new provider for minimax and qwen portals (#203)

Co-authored-by: Haze <709547807@qq.com>
This commit is contained in:
paisley
2026-02-27 14:59:37 +08:00
committed by GitHub
Unverified
parent 5d548da2e6
commit f70d5b0c28
12 changed files with 154 additions and 51 deletions

View File

@@ -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 });
}

View File

@@ -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;

View File

@@ -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',

View File

@@ -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);