mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-07 23:23:52 +08:00
feat(ai): add MiniMax regional endpoint options
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: [] },
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user