feat(ai): add MiniMax regional endpoint options

This commit is contained in:
octo-patch
2026-08-01 13:25:16 +08:00
parent 4635d688c3
commit 4f12dd8436
5 changed files with 150 additions and 10 deletions

View File

@@ -346,6 +346,8 @@ export const AISettingsContent: React.FC<AISettingsContentProps> = ({ active, da
presetKey: matchedPreset.key,
presetBackendType: matchedPreset.backendType,
presetFixedApiFormat: matchedPreset.fixedApiFormat,
presetEndpoints: matchedPreset.endpoints,
valuesBaseUrl: editableProvider.baseUrl,
valuesApiFormat: editableProvider.apiFormat,
});
applyProviderEditorSession(buildEditProviderEditorSession({
@@ -412,12 +414,15 @@ export const AISettingsContent: React.FC<AISettingsContentProps> = ({ active, da
const finalBaseUrl = resolvePresetBaseURL({
presetKey: values.presetKey,
presetDefaultBaseUrl: preset.defaultBaseUrl,
presetEndpoints: preset.endpoints,
valuesBaseUrl: values.baseUrl,
});
const resolvedTransport = resolvePresetTransport({
presetKey: values.presetKey,
presetBackendType: preset.backendType,
presetFixedApiFormat: preset.fixedApiFormat,
presetEndpoints: preset.endpoints,
valuesBaseUrl: finalBaseUrl,
valuesApiFormat: values.apiFormat,
});
const apiKeyInput = usesLocalCLI ? '' : values.apiKey;
@@ -704,6 +709,7 @@ export const AISettingsContent: React.FC<AISettingsContentProps> = ({ active, da
const finalBaseUrl = resolvePresetBaseURL({
presetKey: values.presetKey || 'openai',
presetDefaultBaseUrl: preset.defaultBaseUrl,
presetEndpoints: preset.endpoints,
valuesBaseUrl: values.baseUrl,
});
const { model: finalModel, models: resolvedModels } = resolvePresetModelSelection({
@@ -717,6 +723,8 @@ export const AISettingsContent: React.FC<AISettingsContentProps> = ({ active, da
presetKey: values.presetKey || 'openai',
presetBackendType: preset.backendType,
presetFixedApiFormat: preset.fixedApiFormat,
presetEndpoints: preset.endpoints,
valuesBaseUrl: finalBaseUrl,
valuesApiFormat: values.apiFormat,
});
const allowEmptySecret = values.presetKey === 'codebuddy';
@@ -763,6 +771,8 @@ export const AISettingsContent: React.FC<AISettingsContentProps> = ({ active, da
presetKey,
presetBackendType: preset.backendType,
presetFixedApiFormat: preset.fixedApiFormat,
presetEndpoints: preset.endpoints,
valuesBaseUrl: preset.defaultBaseUrl,
valuesApiFormat: form.getFieldValue('apiFormat'),
});
const { model: presetModel, models: presetModels } = resolvePresetModelSelection({

View File

@@ -6,7 +6,11 @@ import type { FormInstance } from 'antd/es/form';
import type { AIProviderConfig } from '../../types';
import { t as catalogTranslate } from '../../i18n/catalog';
import { useOptionalI18n } from '../../i18n/provider';
import { isLocalCLISubscriptionProvider, type ProviderPresetCandidate } from '../../utils/aiProviderPresets';
import {
isLocalCLISubscriptionProvider,
type ProviderPresetCandidate,
type ProviderPresetEndpoint,
} from '../../utils/aiProviderPresets';
import { isProviderSecretRequirementSatisfied } from '../../utils/providerSecretDraft';
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
import {
@@ -23,6 +27,7 @@ export interface AISettingsProviderPresetOption {
icon: React.ReactNode;
desc: string;
defaultBaseUrl: string;
endpoints?: ProviderPresetEndpoint[];
defaultModel?: string;
models?: string[];
authMode?: AIProviderConfig['authMode'];
@@ -113,6 +118,7 @@ const AISettingsProvidersSection: React.FC<AISettingsProvidersSectionProps> = ({
const copy = (key: string) => (i18n?.t ?? ((catalogKey) => catalogTranslate('en-US', catalogKey)))(key);
const presetKeyFromForm = watchedPresetKey || (editingProvider as (AIProviderConfig & { presetKey?: string }) | null)?.presetKey || 'openai';
const presetFromForm = providerPresets.find((preset) => preset.key === presetKeyFromForm);
const endpointOptions = presetFromForm?.endpoints || [];
const usesLocalCLI = presetFromForm?.authMode === 'local-cli';
const supportsAdvancedEndpoint = presetKeyFromForm === 'custom' || presetKeyFromForm === 'ollama' || presetKeyFromForm === 'codebuddy' || presetKeyFromForm === 'cursor';
const supportsModelList = supportsAdvancedEndpoint || usesLocalCLI;
@@ -499,7 +505,31 @@ const AISettingsProvidersSection: React.FC<AISettingsProvidersSectionProps> = ({
</Form.Item>
)}
{supportsAdvancedEndpoint && (
{endpointOptions.length > 0 ? (
<Form.Item
label={<span style={{ fontWeight: 500, color: overlayTheme.titleText }}>{copy('ai_settings.form.api_endpoint')}</span>}
name="baseUrl"
rules={[{ required: true, message: copy('ai_settings.form.api_endpoint_required') }]}
style={{ marginBottom: 0 }}
>
<Select
showSearch
optionFilterProp="label"
size="middle"
options={endpointOptions.map((endpoint) => ({
label: endpoint.baseUrl,
value: endpoint.baseUrl,
}))}
onChange={(baseUrl) => {
const endpoint = endpointOptions.find((item) => item.baseUrl === baseUrl);
if (endpoint) {
form.setFieldValue('type', endpoint.backendType);
}
}}
style={{ width: '100%' }}
/>
</Form.Item>
) : supportsAdvancedEndpoint && (
<Form.Item
label={<span style={{ fontWeight: 500, color: overlayTheme.titleText }}>{copy('ai_settings.form.api_endpoint')}</span>}
name="baseUrl"

View File

@@ -1,10 +1,15 @@
import { describe, expect, it } from 'vitest';
import { QWEN_CODING_PLAN_ANTHROPIC_BASE_URL } from '../../utils/aiProviderPresets';
import {
QWEN_CODING_PLAN_ANTHROPIC_BASE_URL,
resolvePresetBaseURL,
resolvePresetTransport,
} from '../../utils/aiProviderPresets';
import { t as translateCatalog } from '../../i18n/catalog';
import {
EMPTY_MCP_SERVER,
EMPTY_SKILL,
MINIMAX_ENDPOINTS,
PROVIDER_PRESETS,
findPreset,
localizeProviderPresets,
@@ -48,6 +53,42 @@ describe('aiSettingsModalConfig', () => {
expect(preset.key).toBe('cursor');
});
it('supports every configured MiniMax region and protocol endpoint', () => {
const preset = findPreset('minimax');
expect(preset.defaultBaseUrl).toBe('https://api.minimax.io/anthropic');
expect(MINIMAX_ENDPOINTS).toEqual([
{ backendType: 'anthropic', baseUrl: 'https://api.minimax.io/anthropic' },
{ backendType: 'anthropic', baseUrl: 'https://api.minimaxi.com/anthropic' },
{ backendType: 'openai', baseUrl: 'https://api.minimax.io/v1' },
{ backendType: 'openai', baseUrl: 'https://api.minimaxi.com/v1' },
]);
for (const endpoint of MINIMAX_ENDPOINTS) {
expect(matchProviderPreset({
type: endpoint.backendType,
baseUrl: endpoint.baseUrl,
}).key).toBe('minimax');
}
const selectedBaseUrl = 'https://api.minimaxi.com/v1';
expect(resolvePresetBaseURL({
presetKey: preset.key,
presetDefaultBaseUrl: preset.defaultBaseUrl,
presetEndpoints: preset.endpoints,
valuesBaseUrl: selectedBaseUrl,
})).toBe(selectedBaseUrl);
expect(resolvePresetTransport({
presetKey: preset.key,
presetBackendType: preset.backendType,
presetEndpoints: preset.endpoints,
valuesBaseUrl: selectedBaseUrl,
})).toEqual({
type: 'openai',
apiFormat: undefined,
});
});
it('matches local Codex and Claude subscriptions without confusing Qwen Claude CLI', () => {
expect(matchProviderPreset({
type: 'custom',

View File

@@ -21,6 +21,7 @@ import {
QWEN_CODING_PLAN_MODELS,
resolveProviderPresetKey,
type ProviderPresetCandidate,
type ProviderPresetEndpoint,
} from '../../utils/aiProviderPresets';
export interface ProviderPreset {
@@ -35,10 +36,18 @@ export interface ProviderPreset {
fixedApiFormat?: string;
authMode?: AIProviderAuthMode;
defaultBaseUrl: string;
endpoints?: ProviderPresetEndpoint[];
defaultModel: string;
models: string[];
}
export const MINIMAX_ENDPOINTS: ProviderPresetEndpoint[] = [
{ backendType: 'anthropic', baseUrl: 'https://api.minimax.io/anthropic' },
{ backendType: 'anthropic', baseUrl: 'https://api.minimaxi.com/anthropic' },
{ backendType: 'openai', baseUrl: 'https://api.minimax.io/v1' },
{ backendType: 'openai', baseUrl: 'https://api.minimaxi.com/v1' },
];
export const PROVIDER_PRESETS: ProviderPreset[] = [
{ key: 'openai', label: 'OpenAI', labelKey: 'ai_settings.provider_preset.openai.label', icon: <ApiOutlined />, desc: 'GPT-5.4 / 5.3 series', descKey: 'ai_settings.provider_preset.openai.desc', color: '#10b981', backendType: 'openai', defaultBaseUrl: 'https://api.openai.com/v1', defaultModel: 'gpt-4o', models: [] },
{ key: 'codex', label: 'Codex Subscription', labelKey: 'ai_settings.provider_preset.codex.label', icon: <ApiOutlined />, desc: 'Local Codex CLI / ChatGPT subscription login', descKey: 'ai_settings.provider_preset.codex.desc', color: '#111827', backendType: 'custom', fixedApiFormat: 'codex-cli', authMode: 'local-cli', defaultBaseUrl: '', defaultModel: '', models: [] },
@@ -52,7 +61,20 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
{ key: 'gemini', label: 'Gemini', labelKey: 'ai_settings.provider_preset.gemini.label', icon: <CloudOutlined />, desc: 'Gemini 3.1 / 2.5 series', descKey: 'ai_settings.provider_preset.gemini.desc', color: '#059669', backendType: 'gemini', defaultBaseUrl: 'https://generativelanguage.googleapis.com', defaultModel: 'gemini-2.5-flash', models: [] },
{ key: 'volcengine-ark', label: 'Volcengine Ark', labelKey: 'ai_settings.provider_preset.volcengine_ark.label', icon: <CloudOutlined />, desc: 'Ark general inference / Doubao models', descKey: 'ai_settings.provider_preset.volcengine_ark.desc', color: '#0ea5e9', backendType: 'openai', defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', defaultModel: '', models: [] },
{ key: 'volcengine-coding', label: 'Volcengine Coding Plan', labelKey: 'ai_settings.provider_preset.volcengine_coding.label', icon: <CloudOutlined />, desc: 'Ark Code / Coding Plan', descKey: 'ai_settings.provider_preset.volcengine_coding.desc', color: '#0284c7', backendType: 'openai', defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', defaultModel: '', models: [] },
{ key: 'minimax', label: 'MiniMax', labelKey: 'ai_settings.provider_preset.minimax.label', icon: <ExperimentOutlined />, desc: 'M3 / M2.7 series (Anthropic-compatible)', descKey: 'ai_settings.provider_preset.minimax.desc', color: '#e11d48', backendType: 'anthropic', defaultBaseUrl: 'https://api.minimaxi.com/anthropic', defaultModel: 'MiniMax-M3', models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'] },
{
key: 'minimax',
label: 'MiniMax',
labelKey: 'ai_settings.provider_preset.minimax.label',
icon: <ExperimentOutlined />,
desc: 'M3 / M2.7 series (Anthropic-compatible)',
descKey: 'ai_settings.provider_preset.minimax.desc',
color: '#e11d48',
backendType: 'anthropic',
defaultBaseUrl: MINIMAX_ENDPOINTS[0].baseUrl,
endpoints: MINIMAX_ENDPOINTS,
defaultModel: 'MiniMax-M3',
models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'],
},
{ key: 'codebuddy', label: 'CodeBuddy', labelKey: 'ai_settings.provider_preset.codebuddy.label', icon: <ApiOutlined />, desc: 'Local CodeBuddy CLI / official login session', descKey: 'ai_settings.provider_preset.codebuddy.desc', color: '#2563eb', backendType: 'custom', fixedApiFormat: 'codebuddy-cli', defaultBaseUrl: '', defaultModel: '', models: [] },
{ key: 'cursor', label: 'Cursor', labelKey: 'ai_settings.provider_preset.cursor.label', icon: <ApiOutlined />, desc: 'Cloud Agents API / official API Key', descKey: 'ai_settings.provider_preset.cursor.desc', color: '#7c3aed', backendType: 'custom', fixedApiFormat: 'cursor-agent', defaultBaseUrl: 'https://api.cursor.com/v1', defaultModel: '', models: [] },
{ key: 'ollama', label: 'Ollama', labelKey: 'ai_settings.provider_preset.ollama.label', icon: <AppstoreOutlined />, desc: 'Locally deployed open-source models', descKey: 'ai_settings.provider_preset.ollama.desc', color: '#78716c', backendType: 'openai', defaultBaseUrl: 'http://localhost:11434/v1', defaultModel: 'llama3', models: [] },

View File

@@ -33,9 +33,15 @@ export interface ResolvePresetModelSelectionResult {
models: string[];
}
export interface ProviderPresetEndpoint {
backendType: AIProviderType;
baseUrl: string;
}
export interface ResolvePresetBaseURLInput {
presetKey: string;
presetDefaultBaseUrl: string;
presetEndpoints?: ProviderPresetEndpoint[];
valuesBaseUrl?: string;
}
@@ -43,6 +49,8 @@ export interface ResolvePresetTransportInput {
presetKey?: string;
presetBackendType: AIProviderType;
presetFixedApiFormat?: string;
presetEndpoints?: ProviderPresetEndpoint[];
valuesBaseUrl?: string;
valuesApiFormat?: string;
}
@@ -55,6 +63,7 @@ export interface ProviderPresetMatcher {
key: string;
backendType: AIProviderType;
defaultBaseUrl: string;
endpoints?: ProviderPresetEndpoint[];
fixedApiFormat?: string;
authMode?: AIProviderAuthMode;
}
@@ -166,12 +175,17 @@ export const resolveProviderPresetKey = (
return formatOnlyPreset.key;
}
const exactPreset = presets.find((preset) =>
preset.backendType === provider.type
&& fingerprint !== ''
&& fingerprint === getProviderFingerprint(preset.defaultBaseUrl)
&& (!preset.fixedApiFormat || preset.fixedApiFormat === provider.apiFormat),
);
const exactPreset = presets.find((preset) => {
const matchesDefaultEndpoint = preset.backendType === provider.type
&& fingerprint === getProviderFingerprint(preset.defaultBaseUrl);
const matchesConfiguredEndpoint = preset.endpoints?.some((endpoint) =>
endpoint.backendType === provider.type
&& fingerprint === getProviderFingerprint(endpoint.baseUrl),
);
return fingerprint !== ''
&& (matchesDefaultEndpoint || matchesConfiguredEndpoint)
&& (!preset.fixedApiFormat || preset.fixedApiFormat === provider.apiFormat);
});
if (exactPreset) {
return exactPreset.key;
}
@@ -228,11 +242,20 @@ export const resolvePresetModelSelection = ({
export const resolvePresetBaseURL = ({
presetKey,
presetDefaultBaseUrl,
presetEndpoints,
valuesBaseUrl,
}: ResolvePresetBaseURLInput): string => {
if (CUSTOM_LIKE_PRESET_KEYS.has(presetKey)) {
return valuesBaseUrl || presetDefaultBaseUrl;
}
const valuesFingerprint = getProviderFingerprint(valuesBaseUrl);
const matchesConfiguredEndpoint = presetEndpoints?.some((endpoint) =>
valuesFingerprint !== ''
&& valuesFingerprint === getProviderFingerprint(endpoint.baseUrl),
);
if (matchesConfiguredEndpoint) {
return valuesBaseUrl || presetDefaultBaseUrl;
}
return presetDefaultBaseUrl;
};
@@ -240,8 +263,22 @@ export const resolvePresetTransport = ({
presetKey,
presetBackendType,
presetFixedApiFormat,
presetEndpoints,
valuesBaseUrl,
valuesApiFormat,
}: ResolvePresetTransportInput): ResolvePresetTransportResult => {
const valuesFingerprint = getProviderFingerprint(valuesBaseUrl);
const selectedEndpoint = presetEndpoints?.find((endpoint) =>
valuesFingerprint !== ''
&& valuesFingerprint === getProviderFingerprint(endpoint.baseUrl),
);
if (selectedEndpoint) {
return {
type: selectedEndpoint.backendType,
apiFormat: undefined,
};
}
if (presetFixedApiFormat) {
return {
type: presetBackendType,