Skip to content

Commit dcffff4

Browse files
authored
feat: Support a new LLM provider type: vertex (higress-group#540)
1 parent ff3635b commit dcffff4

File tree

8 files changed

+546
-20
lines changed

8 files changed

+546
-20
lines changed

backend/sdk/src/main/java/com/alibaba/higress/sdk/model/ai/LlmProviderType.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,6 @@ private LlmProviderType() {}
7575
public static final String TOGETHER_AI = "together-ai";
7676

7777
public static final String BEDROCK = "bedrock";
78+
79+
public static final String VERTEX = "vertex";
7880
}

backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/LlmProviderHandler.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313
package com.alibaba.higress.sdk.service.ai;
1414

15+
import java.util.List;
1516
import java.util.Map;
1617

1718
import com.alibaba.higress.sdk.model.ServiceSource;
@@ -39,5 +40,9 @@ default LlmProvider createProvider() {
3940

4041
ServiceSource buildServiceSource(String providerName, Map<String, Object> providerConfig);
4142

43+
default List<ServiceSource> getExtraServiceSources(String providerName, Map<String, Object> providerConfig, boolean forDelete) {
44+
return null;
45+
}
46+
4247
UpstreamService buildUpstreamService(String providerName, Map<String, Object> providerConfig);
4348
}

backend/sdk/src/main/java/com/alibaba/higress/sdk/service/ai/LlmProviderServiceImpl.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.stream.Collectors;
2727
import java.util.stream.Stream;
2828

29-
import com.alibaba.higress.sdk.util.MapUtil;
3029
import org.apache.commons.collections4.CollectionUtils;
3130
import org.apache.commons.collections4.MapUtils;
3231
import org.apache.commons.lang3.StringUtils;
@@ -45,6 +44,7 @@
4544
import com.alibaba.higress.sdk.service.ServiceSourceService;
4645
import com.alibaba.higress.sdk.service.WasmPluginInstanceService;
4746
import com.alibaba.higress.sdk.service.kubernetes.crd.mcp.V1McpBridge;
47+
import com.alibaba.higress.sdk.util.MapUtil;
4848

4949
@SuppressWarnings("unchecked")
5050
public class LlmProviderServiceImpl implements LlmProviderService {
@@ -80,7 +80,8 @@ public class LlmProviderServiceImpl implements LlmProviderService {
8080
new DefaultLlmProviderHandler(LlmProviderType.DOUBAO, "ark.cn-beijing.volces.com", 443,
8181
V1McpBridge.PROTOCOL_HTTPS),
8282
new DefaultLlmProviderHandler(LlmProviderType.COZE, "api.coze.cn", 443, V1McpBridge.PROTOCOL_HTTPS),
83-
new BedrockLlmProviderHandler()).collect(Collectors.toMap(LlmProviderHandler::getType, p -> p));
83+
new BedrockLlmProviderHandler(), new VertexLlmProviderHandler())
84+
.collect(Collectors.toMap(LlmProviderHandler::getType, p -> p));
8485
}
8586

8687
private final ServiceSourceService serviceSourceService;
@@ -150,6 +151,14 @@ public LlmProvider addOrUpdate(LlmProvider provider) {
150151

151152
ServiceSource serviceSource = handler.buildServiceSource(provider.getName(), providerConfig);
152153

154+
List<ServiceSource> extraServiceSources =
155+
handler.getExtraServiceSources(provider.getName(), providerConfig, false);
156+
if (CollectionUtils.isNotEmpty(extraServiceSources)) {
157+
for (ServiceSource extraSource : extraServiceSources) {
158+
serviceSourceService.addOrUpdate(extraSource);
159+
}
160+
}
161+
153162
UpstreamService upstreamService = handler.buildUpstreamService(provider.getName(), providerConfig);
154163
WasmPluginInstance serviceInstance = new WasmPluginInstance();
155164
serviceInstance.setPluginName(instance.getPluginName());
@@ -232,6 +241,14 @@ public void delete(String providerName) {
232241
BuiltInPluginName.AI_PROXY);
233242
ServiceSource serviceSource = handler.buildServiceSource(providerName, deletedProvider);
234243
serviceSourceService.delete(serviceSource.getName());
244+
245+
List<ServiceSource> extraServiceSources =
246+
handler.getExtraServiceSources(providerName, deletedProvider, true);
247+
if (CollectionUtils.isNotEmpty(extraServiceSources)) {
248+
for (ServiceSource extraSource : extraServiceSources) {
249+
serviceSourceService.delete(extraSource.getName());
250+
}
251+
}
235252
}
236253
}
237254

@@ -268,7 +285,7 @@ private SortedMap<String, LlmProvider> getProviders() {
268285
if (!(providersObj instanceof List<?>)) {
269286
return new TreeMap<>();
270287
}
271-
List<?> providerList= (List<?>)providersObj;
288+
List<?> providerList = (List<?>)providersObj;
272289
SortedMap<String, LlmProvider> providers = new TreeMap<>();
273290
for (Object providerObj : providerList) {
274291
if (!(providerObj instanceof Map<?, ?>)) {
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright (c) 2022-2023 Alibaba Group Holding Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*/
13+
package com.alibaba.higress.sdk.service.ai;
14+
15+
import java.util.Collections;
16+
import java.util.List;
17+
import java.util.Locale;
18+
import java.util.Map;
19+
import java.util.function.BiConsumer;
20+
21+
import org.apache.commons.collections4.MapUtils;
22+
import org.apache.commons.lang3.StringUtils;
23+
24+
import com.alibaba.fastjson.JSON;
25+
import com.alibaba.fastjson.JSONException;
26+
import com.alibaba.fastjson.JSONObject;
27+
import com.alibaba.higress.sdk.constant.HigressConstants;
28+
import com.alibaba.higress.sdk.exception.ValidationException;
29+
import com.alibaba.higress.sdk.model.ServiceSource;
30+
import com.alibaba.higress.sdk.model.ai.LlmProviderEndpoint;
31+
import com.alibaba.higress.sdk.model.ai.LlmProviderType;
32+
import com.alibaba.higress.sdk.service.kubernetes.crd.mcp.V1McpBridge;
33+
34+
public class VertexLlmProviderHandler extends AbstractLlmProviderHandler {
35+
36+
private static final String VERTEX_AUTH_KEY_KEY = "vertexAuthKey";
37+
private static final String VERTEX_REGION_KEY = "vertexRegion";
38+
private static final String VERTEX_PROJECT_ID_KEY = "vertexProjectId";
39+
private static final String VERTEX_AUTH_SERVICE_NAME_KEY = "vertexAuthServiceName";
40+
private static final String VERTEX_TOKEN_REFRESH_AHEAD_KEY = "vertexTokenRefreshAhead";
41+
private static final String GEMINI_SAFETY_SETTING_KEY = "geminiSafetySetting";
42+
43+
private static final String DOMAIN_FORMAT = "%s-aiplatform.googleapis.com";
44+
45+
private static final String DEFAULT_AUTH_SERVICE_NAME =
46+
"vertex-auth" + HigressConstants.INTERNAL_RESOURCE_NAME_SUFFIX;
47+
private static final String AUTH_SERVICE_DOMAIN = "oauth2.googleapis.com";
48+
private static final List<ServiceSource> EXTRA_SERVICE_SOURCES;
49+
50+
static {
51+
ServiceSource authServiceSource = new ServiceSource();
52+
authServiceSource.setName(DEFAULT_AUTH_SERVICE_NAME);
53+
authServiceSource.setType(V1McpBridge.REGISTRY_TYPE_DNS);
54+
authServiceSource.setProtocol(V1McpBridge.PROTOCOL_HTTPS);
55+
authServiceSource.setPort(443);
56+
authServiceSource.setDomain(AUTH_SERVICE_DOMAIN);
57+
EXTRA_SERVICE_SOURCES = Collections.singletonList(authServiceSource);
58+
}
59+
60+
@Override
61+
public String getType() {
62+
return LlmProviderType.VERTEX;
63+
}
64+
65+
@Override
66+
public void normalizeConfigs(Map<String, Object> configurations) {
67+
if (MapUtils.isEmpty(configurations)) {
68+
throw new ValidationException("Missing Vertex specific configurations.");
69+
}
70+
71+
String region = MapUtils.getString(configurations, VERTEX_REGION_KEY);
72+
if (StringUtils.isEmpty(region)) {
73+
throw new ValidationException(VERTEX_REGION_KEY + " cannot be empty.");
74+
}
75+
configurations.put(VERTEX_REGION_KEY, region.toLowerCase(Locale.ROOT));
76+
77+
String projectId = MapUtils.getString(configurations, VERTEX_PROJECT_ID_KEY);
78+
if (StringUtils.isEmpty(projectId)) {
79+
throw new ValidationException(VERTEX_PROJECT_ID_KEY + " cannot be empty.");
80+
}
81+
82+
String authKey = MapUtils.getString(configurations, VERTEX_AUTH_KEY_KEY);
83+
if (StringUtils.isEmpty(authKey)) {
84+
throw new ValidationException(VERTEX_AUTH_KEY_KEY + " cannot be empty.");
85+
}
86+
JSONObject authKeyObject;
87+
try {
88+
authKeyObject = JSON.parseObject(authKey);
89+
} catch (JSONException ex) {
90+
throw new ValidationException(VERTEX_AUTH_KEY_KEY + " must contain a valid JSON object.", ex);
91+
}
92+
final BiConsumer<JSONObject, String> ensureJsonStringProperty = (jsonObject, key) -> {
93+
Object value = jsonObject.get(key);
94+
if (!(value instanceof String)) {
95+
throw new ValidationException(
96+
VERTEX_AUTH_KEY_KEY + " must contain a valid JSON object with a string property: " + key);
97+
}
98+
};
99+
ensureJsonStringProperty.accept(authKeyObject, "client_email");
100+
ensureJsonStringProperty.accept(authKeyObject, "private_key_id");
101+
ensureJsonStringProperty.accept(authKeyObject, "private_key");
102+
ensureJsonStringProperty.accept(authKeyObject, "token_uri");
103+
104+
Integer tokenRefreshAhead = MapUtils.getInteger(configurations, VERTEX_TOKEN_REFRESH_AHEAD_KEY);
105+
if (tokenRefreshAhead != null && tokenRefreshAhead < 0) {
106+
throw new ValidationException(VERTEX_TOKEN_REFRESH_AHEAD_KEY + " must be a non-negative number.");
107+
}
108+
109+
Object rawGeminiSafetySetting = configurations.get(GEMINI_SAFETY_SETTING_KEY);
110+
if (rawGeminiSafetySetting != null) {
111+
if (!(rawGeminiSafetySetting instanceof Map)) {
112+
throw new ValidationException(GEMINI_SAFETY_SETTING_KEY + " must be an object.");
113+
}
114+
Map<?, ?> geminiSafetySetting = (Map<?, ?>)rawGeminiSafetySetting;
115+
for (Map.Entry<?, ?> entry : geminiSafetySetting.entrySet()) {
116+
if (!(entry.getKey() instanceof String) || !(entry.getValue() instanceof String)) {
117+
throw new ValidationException(
118+
GEMINI_SAFETY_SETTING_KEY + " must be an object with string key-value pairs.");
119+
}
120+
}
121+
}
122+
123+
configurations.put(VERTEX_AUTH_SERVICE_NAME_KEY, DEFAULT_AUTH_SERVICE_NAME);
124+
}
125+
126+
@Override
127+
protected List<LlmProviderEndpoint> getProviderEndpoints(Map<String, Object> providerConfig) {
128+
String region = MapUtils.getString(providerConfig, VERTEX_REGION_KEY);
129+
if (StringUtils.isEmpty(region)) {
130+
throw new ValidationException(VERTEX_REGION_KEY + " cannot be empty.");
131+
}
132+
String domain = String.format(DOMAIN_FORMAT, region);
133+
return Collections.singletonList(new LlmProviderEndpoint(V1McpBridge.PROTOCOL_HTTPS, domain, 443, "/"));
134+
}
135+
136+
@Override
137+
public List<ServiceSource> getExtraServiceSources(String providerName, Map<String, Object> providerConfig,
138+
boolean forDelete) {
139+
return forDelete ? Collections.emptyList() : EXTRA_SERVICE_SOURCES;
140+
}
141+
}

frontend/src/locales/en-US/translation.json

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,14 @@
254254
"openaiCustomUrl": "Custom OpenAI Service Base URL",
255255
"awsRegion": "AWS Region",
256256
"awsAccessKey": "AWS Access Key ID",
257-
"awsSecretKey": "AWS Secret Access Key"
257+
"awsSecretKey": "AWS Secret Access Key",
258+
"vertexRegion": "Google Cloud Region",
259+
"vertexProjectId": "Google Cloud Product ID",
260+
"vertexAuthKey": "Google Cloud Service Account Key",
261+
"vertexTokenRefreshAhead": "Lead time of rereshing Google Cloud Access Token (sec)",
262+
"geminiSafetySettings": "Gemini API Safety Settings",
263+
"geminiSafetyCategory": "Harm Category",
264+
"geminiSafetyThreshold": "Block Threshold"
258265
},
259266
"rules": {
260267
"tokenRequired": "Please input auth token",
@@ -276,7 +283,18 @@
276283
"openaiCustomUrlInconsistentContextPaths": "Inconsistent paths found in custom OpenAI service base URLs",
277284
"awsRegionRequired": "Please input AWS Region",
278285
"awsAccessKeyRequired": "Please input AWS Access Key ID",
279-
"awsSecretKeyRequired": "Please input AWS Secret Access Key"
286+
"awsSecretKeyRequired": "Please input AWS Secret Access Key",
287+
"vertexRegionRequired": "Please input Google Cloud Region",
288+
"vertexProjectIdRequired": "Please input Google Cloud Product ID",
289+
"vertexAuthKeyRequired": "Please input Google Cloud Service Account Key",
290+
"vertexAuthKeyBadFormat": "Invalid Google Cloud service account key format, which must be a JSON string.",
291+
"vertexAuthKeyBadRequiredProperty": "Required property is missing in the Google Cloud service account key: {{key}}",
292+
"geminiSafetyCategoryRequired": "Please input harm category",
293+
"geminiSafetyCategoryDuplicated": "Found duplicated harm categories",
294+
"geminiSafetyThresholdRequired": "Please input block threshold"
295+
},
296+
"tooltips": {
297+
"vertexTokenRefreshAheadTooltip": "The lead time in seconds of refreshing the access token used to send requests to Vertex API. Leave it blank if the token shall only be refreshed after expired."
280298
},
281299
"placeholder": {
282300
"azureServiceUrlPlaceholder": "It shall contain \"/chat/completions\" in the path and \"api-version\" in the query string",
@@ -295,7 +313,8 @@
295313
"content_diffNs": "If the Secret and Higress belong to different namespaces:",
296314
"example": "e.g. ",
297315
"roleConfig": "Higress can read Secret data in any namespace by default. But if the role configuration is modified, please make sure the ServiceAccount bound to Higress Controller is allowed to read the configured Secret resource."
298-
}
316+
},
317+
"addGeminiSafetySetting": "Add a safety setting"
299318
},
300319
"create": "Create AI Service Provider",
301320
"edit": "Edit AI Service Provider",

frontend/src/locales/zh-CN/translation.json

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,14 @@
254254
"openaiCustomUrl": "自定义 OpenAI 服务 BaseURL",
255255
"awsRegion": "AWS Region",
256256
"awsAccessKey": "AWS Access Key ID",
257-
"awsSecretKey": "AWS Secret Access Key"
257+
"awsSecretKey": "AWS Secret Access Key",
258+
"vertexRegion": "Google Cloud Region",
259+
"vertexProjectId": "Google Cloud 项目 ID",
260+
"vertexAuthKey": "Google Cloud 服务账号密钥",
261+
"vertexTokenRefreshAhead": "Google Cloud 访问令牌刷新提前量(秒)",
262+
"geminiSafetySettings": "Gemini API 安全设置",
263+
"geminiSafetyCategory": "危害类别",
264+
"geminiSafetyThreshold": "屏蔽阈值"
258265
},
259266
"rules": {
260267
"tokenRequired": "请输入凭证",
@@ -276,7 +283,18 @@
276283
"openaiCustomUrlInconsistentContextPaths": "各个自定义 OpenAI 服务 BaseURL 所使用的路径不一致",
277284
"awsRegionRequired": "请输入 AWS Region",
278285
"awsAccessKeyRequired": "请输入 AWS Access Key ID",
279-
"awsSecretKeyRequired": "请输入 AWS Secret Access Key"
286+
"awsSecretKeyRequired": "请输入 AWS Secret Access Key",
287+
"vertexRegionRequired": "请输入 Google Cloud Region",
288+
"vertexProjectIdRequired": "请输入 Google Cloud 项目 ID",
289+
"vertexAuthKeyRequired": "请输入 Google Cloud 服务账号密钥",
290+
"vertexAuthKeyBadFormat": "Google Cloud 服务账号密钥的格式不正确,必须为 JSON 格式的字符串",
291+
"vertexAuthKeyBadRequiredProperty": "Google Cloud 服务账号密钥中缺少必要的属性:{{key}}",
292+
"geminiSafetyCategoryRequired": "请输入危害类别",
293+
"geminiSafetyCategoryDuplicated": "存在重复配置的危害类别",
294+
"geminiSafetyThresholdRequired": "请输入屏蔽阈值"
295+
},
296+
"tooltips": {
297+
"vertexTokenRefreshAheadTooltip": "提前刷新用于访问 Vertex API 的访问密钥的时间,单位为秒。留空表示仅在密钥过期时刷新。"
280298
},
281299
"placeholder": {
282300
"azureServiceUrlPlaceholder": "需包含“/chat/completions”路径和“api-version”查询参数",
@@ -295,7 +313,8 @@
295313
"content_diffNs": "若 Secret 与 Higress 在不同的命名空间:",
296314
"example": "例如:",
297315
"roleConfig": "默认配置下,Higress 可以读取任意命名空间下的 Secret 信息。如果你调整过这部分配置,请确保 Higress Controller 的 ServiceAccount 可以读取配置中所引用的 Secret 数据。"
298-
}
316+
},
317+
"addGeminiSafetySetting": "添加安全设置"
299318
},
300319
"create": "创建AI服务提供者",
301320
"edit": "编辑AI服务提供者",

0 commit comments

Comments
 (0)