Skip to content

Commit

Permalink
feat: Support configuring custom service URL in OpenAI type of LLM pr…
Browse files Browse the repository at this point in the history
…oviders
  • Loading branch information
CH3CHO committed Jan 24, 2025
1 parent f038c5e commit 7be85b2
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class LlmProviderServiceImpl implements LlmProviderService {

static {
PROVIDER_HANDLERS = Stream.of(
new DefaultLlmProviderHandler(LlmProviderType.OPENAI, "api.openai.com", 443, V1McpBridge.PROTOCOL_HTTPS),
new OpenaiLlmProviderHandler(),
new DefaultLlmProviderHandler(LlmProviderType.MOONSHOT, "api.moonshot.cn", 443, V1McpBridge.PROTOCOL_HTTPS),
new DefaultLlmProviderHandler(LlmProviderType.QWEN, "dashscope.aliyuncs.com", 443,
V1McpBridge.PROTOCOL_HTTPS),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright (c) 2022-2023 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.alibaba.higress.sdk.service.ai;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;

import com.alibaba.higress.sdk.exception.ValidationException;
import com.alibaba.higress.sdk.model.ai.LlmProviderType;
import com.alibaba.higress.sdk.service.kubernetes.crd.mcp.V1McpBridge;
import com.alibaba.higress.sdk.util.ValidateUtil;

public class OpenaiLlmProviderHandler extends AbstractLlmProviderHandler {

private static final String CUSTOM_URL_KEY = "openaiCustomUrl";

private static final String DEFAULT_REGISTRY_TYPE = V1McpBridge.REGISTRY_TYPE_DNS;
private static final String DEFAULT_SERVICE_DOMAIN = "api.openai.com";
private static final int DEFAULT_SERVICE_PORT = 443;
private static final String DEFAULT_SERVICE_PROTOCOL = V1McpBridge.PROTOCOL_HTTPS;

@Override
public String getType() {
return LlmProviderType.OPENAI;
}

@Override
public void validateConfig(Map<String, Object> configurations) {
if (MapUtils.isEmpty(configurations)) {
return;
}
URI uri = getCustomUri(configurations);
if (uri != null) {
String scheme = uri.getScheme();
if (StringUtils.isEmpty(scheme)) {
throw new ValidationException("Custom service URL must have a scheme.");
}
scheme = scheme.toLowerCase(Locale.ROOT);
if (!scheme.equals(V1McpBridge.PROTOCOL_HTTP) && !scheme.equals(V1McpBridge.PROTOCOL_HTTPS)) {
throw new ValidationException("Custom service URL must have a valid scheme.");
}
}
}

@Override
protected String getServiceRegistryType(Map<String, Object> providerConfig) {
URI uri = getCustomUri(providerConfig);
if (uri == null) {
return DEFAULT_REGISTRY_TYPE;
}
if (ValidateUtil.checkIpAddress(uri.getHost())) {
return V1McpBridge.REGISTRY_TYPE_STATIC;
}
return V1McpBridge.REGISTRY_TYPE_DNS;
}

@Override
protected String getServiceDomain(Map<String, Object> providerConfig) {
URI uri = getCustomUri(providerConfig);
return uri != null ? uri.getHost() : DEFAULT_SERVICE_DOMAIN;
}

@Override
protected int getServicePort(Map<String, Object> providerConfig) {
URI uri = getCustomUri(providerConfig);
if (uri == null){
return DEFAULT_SERVICE_PORT;
}
int port = uri.getPort();
if (port != -1){
return port;
}
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<String, Object> providerConfig) {
URI uri = getCustomUri(providerConfig);
if (uri == null){
return DEFAULT_SERVICE_PROTOCOL;
}
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 getCustomUri(Map<String, Object> providerConfig) {
if (MapUtils.isEmpty(providerConfig)) {
return null;
}
Object customUrlObject = providerConfig.get(CUSTOM_URL_KEY);
if (!(customUrlObject instanceof String customUrl)) {
throw new ValidationException(CUSTOM_URL_KEY + " must be a string.");
}
if (StringUtils.isEmpty(customUrl)) {
throw new ValidationException(CUSTOM_URL_KEY + " cannot be empty.");
}
try {
return new URI(customUrl);
} catch (URISyntaxException e) {
throw new ValidationException(CUSTOM_URL_KEY + " is not a valid URL.", e);
}
}
}
14 changes: 11 additions & 3 deletions frontend/src/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,9 @@
"successThreshold": "Min Consecuitive Sucesses to Mark Up a Token",
"azureServiceUrl": "Azure Service URL",
"ollamaServerHost": "Ollama Service Host",
"ollamaServerPort": "Ollama Service Port"
"ollamaServerPort": "Ollama Service Port",
"openaiServerType": "OpenAI Service Type",
"openaiCustomUrl": "Custom OpenAI Service URL"
},
"rules": {
"tokenRequired": "Please input auth token",
Expand All @@ -241,11 +243,17 @@
"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"
"ollamaServerPortRequired": "Please input Ollama service port",
"openaiCustomUrlRequired": "Please input a valid custom OpenAI service URL"
},
"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"
"ollamaServerHostPlaceholder": "Please input a hostname, domain name or IP address",
"openaiCustomUrlPlaceholder": "Sample: http://www.example.com/myai/v1/chat/completions"
},
"openaiServerType": {
"official": "OpenAI Official Service",
"custom": "Custom Service"
}
},
"create": "Create AI Service Provider",
Expand Down
16 changes: 12 additions & 4 deletions frontend/src/locales/zh-CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@
"successThreshold": "令牌可用时需满足的最小连续健康检测成功次数",
"azureServiceUrl": "Azure 服务 URL",
"ollamaServerHost": "Ollama 服务主机名",
"ollamaServerPort": "Ollama 服务端口"
"ollamaServerPort": "Ollama 服务端口",
"openaiServerType": "OpenAI 服务类型",
"openaiCustomUrl": "自定义 OpenAI 服务 URL"
},
"rules": {
"tokenRequired": "请输入凭证",
Expand All @@ -240,11 +242,17 @@
"successThresholdRequired": "请输入最小连续健康检测成功次数",
"azureServiceUrlRequired": "请输入 Azure 服务 URL",
"ollamaServerHostRequired": "请输入 Ollama 服务主机名",
"ollamaServerPortRequired": "请输入 Ollama 服务端口"
"ollamaServerPortRequired": "请输入 Ollama 服务端口",
"openaiCustomUrlRequired": "请输入合法的自定义 OpenAI 服务 URL"
},
"placeholder": {
"azureServiceUrlPlaceholder": "需包含“/chat/completions”路径和“api-version”查询参数",
"ollamaServerHostPlaceholder": "请填写机器名、域名或 IP 地址"
"ollamaServerHostPlaceholder": "请填写机器名、域名或 IP 地址",
"openaiCustomUrlPlaceholder": "示例:http://www.example.com/myai/v1/chat/completions"
},
"openaiServerType": {
"official": "OpenAI 官方服务",
"custom": "自定义服务"
}
},
"create": "创建AI服务提供者",
Expand Down Expand Up @@ -594,4 +602,4 @@
"isRequired": "是必填的",
"invalidSchema": "由于 schema 信息无法正常解析,本插件只支持 YAML 编辑方式。"
}
}
}
56 changes: 55 additions & 1 deletion frontend/src/pages/ai/components/ProviderForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => {
const [form] = Form.useForm();
const [failoverEnabled, setFailoverEnabled] = useState(false);
const [providerType, setProviderType] = useState<string | null>();
const [openaiServerType, setOpenaiServerType] = useState<string | null>();
const [providerConfig, setProviderConfig] = useState<object | null>();

useEffect(() => {
Expand Down Expand Up @@ -50,12 +51,19 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => {
rawConfigs,
});

if (type === 'openai') {
const openaiServerTypeValue = rawConfigs && rawConfigs.openaiCustomUrl ? 'custom' : 'official';
form.setFieldValue('openaiServerType', openaiServerTypeValue);
onOpenaiServerTypeChanged(openaiServerTypeValue)
}

onProviderTypeChanged(type);
}

return () => {
setFailoverEnabled(false);
onProviderTypeChanged(null);
onOpenaiServerTypeChanged(null)
}
}, [props.value]);

Expand Down Expand Up @@ -87,6 +95,10 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => {
},
}));

function onOpenaiServerTypeChanged(value: string | null) {
setOpenaiServerType(value);
}

function onProviderTypeChanged(value: string | null) {
setProviderType(value);
setProviderConfig(value ? aiModelProviders.find(p => p.value === value) : null);
Expand Down Expand Up @@ -162,7 +174,7 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => {
placeholder={t('llmProvider.providerForm.rules.protocol')}
>
{protocolList.map(item => (
<Select.Option value={item.value}>
<Select.Option key={item.value} value={item.value}>
{item.label}
</Select.Option>
))}
Expand Down Expand Up @@ -240,6 +252,48 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => {
<Switch onChange={e => setFailoverEnabled(e)} />
</Form.Item>

{
providerType === 'openai' && (
<>
<Form.Item
label={t('llmProvider.providerForm.label.openaiServerType')}
required
name="openaiServerType"
initialValue="official"
>
<Select
onChange={onOpenaiServerTypeChanged}
>
<Select.Option value="official">{t("llmProvider.providerForm.openaiServerType.official")}</Select.Option>
<Select.Option value="custom">{t("llmProvider.providerForm.openaiServerType.custom")}</Select.Option>
</Select>
</Form.Item>
{
openaiServerType === "custom" && (
<Form.Item
label={t('llmProvider.providerForm.label.openaiCustomUrl')}
required
name={["rawConfigs", "openaiCustomUrl"]}
rules={[
{
required: true,
pattern: /http(s)?:\/\/.+\/v1\/chat\/completions/,
message: t('llmProvider.providerForm.rules.openaiCustomUrlRequired'),
},
]}
>
<Input
allowClear
type="url"
placeholder={t('llmProvider.providerForm.placeholder.openaiCustomUrlPlaceholder')}
/>
</Form.Item>
)
}
</>
)
}

{
providerType === 'azure' && (
<>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/pages/ai/configs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export const aiModelProviders = [
value: 'gpt-4o-mini',
},
],
getProviderEndpoints: (record) => {
if (!record.rawConfigs) {
return null;
}
return [record.rawConfigs['openaiCustomUrl']];
},
},
{
label: 'Qwen',
Expand Down

0 comments on commit 7be85b2

Please sign in to comment.