diff --git a/backend/console/pom.xml b/backend/console/pom.xml
index 2d695318..3de67f4b 100644
--- a/backend/console/pom.xml
+++ b/backend/console/pom.xml
@@ -16,7 +16,7 @@
16.19.0
- 2.0.0
+ 2.1.0
true
diff --git a/backend/console/src/test/java/com/alibaba/higress/console/FrontEndI18nResourceChecker.java b/backend/console/src/test/java/com/alibaba/higress/console/FrontEndI18nResourceChecker.java
index 452429d2..c9b0f786 100644
--- a/backend/console/src/test/java/com/alibaba/higress/console/FrontEndI18nResourceChecker.java
+++ b/backend/console/src/test/java/com/alibaba/higress/console/FrontEndI18nResourceChecker.java
@@ -42,8 +42,8 @@ public class FrontEndI18nResourceChecker {
private static final String I18N_RESOURCE_FILE_NAME = "translation.json";
private static final List TS_FILE_EXTENSIONS = Arrays.asList(".ts", ".tsx");
- private static final Set IMPLICITLY_USED_RESOURCE_KEYS = Set.of("init.title", "login.title", "aiRoute.edit",
- "tlsCertificate.editTlsCertificate", "serviceSource.editServiceSource", "llmProvider.edit",
+ private static final Set IMPLICITLY_USED_RESOURCE_KEYS = Set.of("index.title", "init.title", "login.title",
+ "aiRoute.edit", "tlsCertificate.editTlsCertificate", "serviceSource.editServiceSource", "llmProvider.edit",
"plugins.editPlugin", "route.editRoute", "domain.editDomain", "consumer.edit");
private static final List IMPLICITLY_USED_RESOURCE_KEY_PREFIXES =
Arrays.asList("menu.", "request.error.", "serviceSource.types.", "llmProvider.providerTypes.",
diff --git a/backend/sdk/src/main/java/com/alibaba/higress/sdk/model/ai/LlmProviderType.java b/backend/sdk/src/main/java/com/alibaba/higress/sdk/model/ai/LlmProviderType.java
index 0f8703d1..1738a259 100644
--- a/backend/sdk/src/main/java/com/alibaba/higress/sdk/model/ai/LlmProviderType.java
+++ b/backend/sdk/src/main/java/com/alibaba/higress/sdk/model/ai/LlmProviderType.java
@@ -66,4 +66,6 @@ private LlmProviderType() {
public static final String DOUBAO = "doubao";
public static final String COZE = "coze";
+
+ public static final String TOGETHER_AI = "together-ai";
}
diff --git a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/AbstractLlmProviderHandler.java b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/AbstractLlmProviderHandler.java
index ec00c684..db3371d3 100644
--- a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/AbstractLlmProviderHandler.java
+++ b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/AbstractLlmProviderHandler.java
@@ -38,6 +38,7 @@
import com.alibaba.higress.sdk.constant.CommonKey;
import com.alibaba.higress.sdk.constant.HigressConstants;
import com.alibaba.higress.sdk.constant.Separators;
+import com.alibaba.higress.sdk.exception.ValidationException;
import com.alibaba.higress.sdk.model.ServiceSource;
import com.alibaba.higress.sdk.model.ai.LlmProvider;
import com.alibaba.higress.sdk.model.ai.LlmProviderProtocol;
@@ -166,6 +167,21 @@ public UpstreamService buildUpstreamService(String providerName, Map providerConfig);
+ protected static int getIntConfig(Map providerConfig, String key) {
+ Object serverPortObj = providerConfig.get(key);
+ if (serverPortObj instanceof Integer) {
+ return (Integer)serverPortObj;
+ }
+ if (serverPortObj instanceof String serverPortStr) {
+ try {
+ return Integer.parseInt(serverPortStr);
+ } catch (NumberFormatException e) {
+ throw new ValidationException(key + " must be a number.");
+ }
+ }
+ throw new ValidationException(key + " must be a number.");
+ }
+
protected static String generateServiceProviderName(String llmProviderName) {
return CommonKey.LLM_SERVICE_NAME_PREFIX + llmProviderName + HigressConstants.INTERNAL_RESOURCE_NAME_SUFFIX;
}
diff --git a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/AzureLlmProviderHandler.java b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/AzureLlmProviderHandler.java
index b23bdbde..9c6e8f6d 100644
--- a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/AzureLlmProviderHandler.java
+++ b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/AzureLlmProviderHandler.java
@@ -14,6 +14,7 @@
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.Locale;
import java.util.Map;
import org.apache.commons.collections4.MapUtils;
@@ -34,20 +35,17 @@ public String getType() {
@Override
public void validateConfig(Map configurations) {
- if (MapUtils.isEmpty(configurations)){
+ if (MapUtils.isEmpty(configurations)) {
throw new ValidationException("Missing Azure specific configurations.");
}
- Object serviceUrlObj = configurations.get(SERVICE_URL_KEY);
- if (!(serviceUrlObj instanceof String serviceUrl)){
- throw new ValidationException(SERVICE_URL_KEY + " must be a string.");
- }
- if (StringUtils.isEmpty(serviceUrl)) {
- throw new ValidationException(SERVICE_URL_KEY + " cannot be empty.");
+ URI uri = getServiceUri(configurations);
+ String scheme = uri.getScheme();
+ if (StringUtils.isEmpty(scheme)) {
+ throw new ValidationException("Azure service URL must have a scheme.");
}
- try {
- new URI(serviceUrl);
- } catch (URISyntaxException e) {
- throw new ValidationException(SERVICE_URL_KEY + " is not a valid URL.", e);
+ scheme = scheme.toLowerCase(Locale.ROOT);
+ if (!scheme.equals(V1McpBridge.PROTOCOL_HTTP) && !scheme.equals(V1McpBridge.PROTOCOL_HTTPS)) {
+ throw new ValidationException("Azure service URL must have a valid scheme.");
}
}
@@ -58,28 +56,52 @@ protected String getServiceRegistryType(Map providerConfig) {
@Override
protected String getServiceDomain(Map providerConfig) {
- if (MapUtils.isEmpty(providerConfig)){
- return "";
- }
- String serviceUrl = (String)providerConfig.get(SERVICE_URL_KEY);
- if (StringUtils.isEmpty(serviceUrl)) {
- return "";
- }
- try {
- URI uri = new URI(serviceUrl);
- return uri.getHost();
- } catch (URISyntaxException e) {
- return null;
- }
+ URI uri = getServiceUri(providerConfig);
+ return uri.getHost();
}
@Override
protected int getServicePort(Map providerConfig) {
- return 443;
+ URI uri = getServiceUri(providerConfig);
+ String scheme = uri.getScheme();
+ if (scheme == null) {
+ return 80;
+ }
+ return switch (scheme.toLowerCase(Locale.ROOT)) {
+ case V1McpBridge.PROTOCOL_HTTP -> 80;
+ case V1McpBridge.PROTOCOL_HTTPS -> 443;
+ default -> 80;
+ };
}
@Override
protected String getServiceProtocol(Map providerConfig) {
- return V1McpBridge.PROTOCOL_HTTPS;
+ URI uri = getServiceUri(providerConfig);
+ String scheme = uri.getScheme();
+ if (scheme == null) {
+ return V1McpBridge.PROTOCOL_HTTP;
+ }
+ return switch (scheme.toLowerCase(Locale.ROOT)) {
+ case V1McpBridge.PROTOCOL_HTTP, V1McpBridge.PROTOCOL_HTTPS -> scheme;
+ default -> V1McpBridge.PROTOCOL_HTTP;
+ };
+ }
+
+ private static URI getServiceUri(Map providerConfig) {
+ if (MapUtils.isEmpty(providerConfig)) {
+ throw new ValidationException("Missing Azure specific configurations.");
+ }
+ Object serviceUrlObj = providerConfig.get(SERVICE_URL_KEY);
+ if (!(serviceUrlObj instanceof String serviceUrl)) {
+ throw new ValidationException(SERVICE_URL_KEY + " must be a string.");
+ }
+ if (StringUtils.isEmpty(serviceUrl)) {
+ throw new ValidationException(SERVICE_URL_KEY + " cannot be empty.");
+ }
+ try {
+ return new URI(serviceUrl);
+ } catch (URISyntaxException e) {
+ throw new ValidationException(SERVICE_URL_KEY + " is not a valid URL.", e);
+ }
}
}
diff --git a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/OllamaLlmProviderHandler.java b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/OllamaLlmProviderHandler.java
index 5171252b..53ae7479 100644
--- a/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/OllamaLlmProviderHandler.java
+++ b/backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/OllamaLlmProviderHandler.java
@@ -46,10 +46,7 @@ public void validateConfig(Map configurations) {
if (StringUtils.isEmpty(serverHost)) {
throw new ValidationException(SERVER_HOST_KEY + " cannot be empty.");
}
- Object serverPortObj = configurations.get(SERVER_PORT_KEY);
- if (!(serverPortObj instanceof Integer serverPort)) {
- throw new ValidationException(SERVER_PORT_KEY + " must be a number.");
- }
+ int serverPort = getIntConfig(configurations, SERVER_PORT_KEY);
if (!ValidateUtil.checkPort(serverPort)) {
throw new ValidationException(SERVER_PORT_KEY + " must be a valid port number.");
}
diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json
index 35b021a5..250ef9e8 100644
--- a/frontend/src/locales/en-US/translation.json
+++ b/frontend/src/locales/en-US/translation.json
@@ -188,7 +188,30 @@
"providerTypes": {
"openai": "OpenAI",
"qwen": "Tongyi Qianwen",
- "moonshot": "Moonshot"
+ "moonshot": "Moonshot",
+ "ai360": "360 Zhinao|",
+ "azure": "Azure OpenAI",
+ "baichuan": "Baichuan AI",
+ "baidu": "ERNIE Bot",
+ "claude": "Anthropic Claude",
+ "cloudflare": "Cloudflare Workers AI",
+ "cohere": "Cohere",
+ "coze": "Coze",
+ "deepl": "DeepL",
+ "deepseek": "DeepSeek",
+ "doubao": "Doubao",
+ "gemini": "Google Gemini",
+ "github": "GitHub Models",
+ "groq": "Groq",
+ "hunyuan": "Tencent Hunyuan",
+ "minimax": "MiniMax",
+ "mistral": "Mistral",
+ "ollama": "Ollama",
+ "together-ai": "Together AI",
+ "stepfun": "Stepfun",
+ "spark": "iFlyTek Spark",
+ "yi": "01.AI",
+ "zhipuai": "Zhipu AI"
},
"providerForm": {
"label": {
@@ -201,7 +224,10 @@
"healthCheckTimeout": "Health Check Request Timeout",
"protocol": "Request Procotol",
"serviceName": "Service Name",
- "successThreshold": "Min Consecuitive Sucesses to Mark Up a Token"
+ "successThreshold": "Min Consecuitive Sucesses to Mark Up a Token",
+ "azureServiceUrl": "Azure Service URL",
+ "ollamaServerHost": "Ollama Service Host",
+ "ollamaServerPort": "Ollama Service Port"
},
"rules": {
"tokenRequired": "Please input auth token",
@@ -212,7 +238,14 @@
"healthCheckIntervalRequired": "Please input health check request interval",
"healthCheckModelRequired": "Please input health check request LLM model",
"protocol": "Please select a request protocol",
- "successThresholdRequired": "Please input min consecuitive sucesses to mark up a token"
+ "successThresholdRequired": "Please input min consecuitive sucesses to mark up a token",
+ "azureServiceUrlRequired": "Please input Azure service URL",
+ "ollamaServerHostRequired": "Please input Ollama service host",
+ "ollamaServerPortRequired": "Please input Ollama service port"
+ },
+ "placeholder": {
+ "azureServiceUrlPlaceholder": "It shall contain \"/chat/completions\" in the path and \"api-version\" in the query string",
+ "ollamaServerHostPlaceholder": "Please input a hostname, domain name or IP address"
}
},
"create": "Create AI Service Provider",
diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json
index 27057297..b6987f2f 100644
--- a/frontend/src/locales/zh-CN/translation.json
+++ b/frontend/src/locales/zh-CN/translation.json
@@ -187,7 +187,30 @@
"providerTypes": {
"openai": "OpenAI",
"qwen": "通义千问",
- "moonshot": "月之暗面"
+ "moonshot": "月之暗面",
+ "ai360": "360智脑",
+ "azure": "Azure OpenAI",
+ "baichuan": "百川智能",
+ "baidu": "文心一言",
+ "claude": "Anthropic Claude",
+ "cloudflare": "Cloudflare Workers AI",
+ "cohere": "Cohere",
+ "coze": "扣子",
+ "deepl": "DeepL",
+ "deepseek": "DeepSeek",
+ "doubao": "豆包",
+ "gemini": "Google Gemini",
+ "github": "GitHub模型",
+ "groq": "Groq",
+ "hunyuan": "混元",
+ "minimax": "MiniMax",
+ "mistral": "Mistral",
+ "ollama": "Ollama",
+ "together-ai": "Together AI",
+ "stepfun": "阶跃星辰",
+ "spark": "星火",
+ "yi": "零一万物",
+ "zhipuai": "智谱AI"
},
"providerForm": {
"label": {
@@ -200,7 +223,10 @@
"healthCheckTimeout": "健康检测请求超时时间",
"protocol": "协议",
"serviceName": "服务名称",
- "successThreshold": "令牌可用时需满足的最小连续健康检测成功次数"
+ "successThreshold": "令牌可用时需满足的最小连续健康检测成功次数",
+ "azureServiceUrl": "Azure 服务 URL",
+ "ollamaServerHost": "Ollama 服务主机名",
+ "ollamaServerPort": "Ollama 服务端口"
},
"rules": {
"tokenRequired": "请输入凭证",
@@ -211,7 +237,14 @@
"healthCheckIntervalRequired": "请输入健康检测请求发起间隔",
"healthCheckModelRequired": "请输入健康检测请求使用的模型名称",
"protocol": "请选择请求协议",
- "successThresholdRequired": "请输入最小连续健康检测成功次数"
+ "successThresholdRequired": "请输入最小连续健康检测成功次数",
+ "azureServiceUrlRequired": "请输入 Azure 服务 URL",
+ "ollamaServerHostRequired": "请输入 Ollama 服务主机名",
+ "ollamaServerPortRequired": "请输入 Ollama 服务端口"
+ },
+ "placeholder": {
+ "azureServiceUrlPlaceholder": "需包含“/chat/completions”路径和“api-version”查询参数",
+ "ollamaServerHostPlaceholder": "请填写机器名、域名或 IP 地址"
}
},
"create": "创建AI服务提供者",
diff --git a/frontend/src/pages/ai/components/ProviderForm/index.tsx b/frontend/src/pages/ai/components/ProviderForm/index.tsx
index 3f1f4b79..c87c5f6c 100644
--- a/frontend/src/pages/ai/components/ProviderForm/index.tsx
+++ b/frontend/src/pages/ai/components/ProviderForm/index.tsx
@@ -2,12 +2,7 @@ import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, InputNumber, Select, Switch } from 'antd';
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { useTranslation } from 'react-i18next';
-
-const providerTypeDisplayName = [
- { key: 'openai', label: 'llmProvider.providerTypes.openai' },
- { key: 'qwen', label: 'llmProvider.providerTypes.qwen' },
- { key: 'moonshot', label: 'llmProvider.providerTypes.moonshot' },
-];
+import { aiModelProviders } from '../../configs';
const protocolList = [
{ label: "openai/v1", value: "openai/v1" },
@@ -17,6 +12,8 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [failoverEnabled, setFailoverEnabled] = useState(false);
+ const [providerType, setProviderType] = useState();
+ const [providerConfig, setProviderConfig] = useState