feat: 添加LLM模型连通性测试功能
- 在 `AiModelServiceImpl` 中添加 `testLlmConnectivity` 方法,支持不同提供商的连通性测试 - 在 `AiModelController` 中添加 `/llm-connectivity-test` API 端点,用于测试 LLM 模型连通性 - 更新 `AiModelService` 接口以包含新的 `testLlmConnectivity` 方法 - 添加相关单元测试以验证连通性测试功能的正确性dev_na
parent
940cc8a939
commit
6a08fb1a3b
|
|
@ -1,8 +1,7 @@
|
||||||
package com.imeeting.controller.biz;
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
|
||||||
import com.imeeting.dto.biz.AiModelDTO;
|
|
||||||
import com.imeeting.dto.biz.AiLocalProfileVO;
|
import com.imeeting.dto.biz.AiLocalProfileVO;
|
||||||
|
import com.imeeting.dto.biz.AiModelDTO;
|
||||||
import com.imeeting.dto.biz.AiModelVO;
|
import com.imeeting.dto.biz.AiModelVO;
|
||||||
import com.imeeting.service.biz.AiModelService;
|
import com.imeeting.service.biz.AiModelService;
|
||||||
import com.unisbase.common.ApiResponse;
|
import com.unisbase.common.ApiResponse;
|
||||||
|
|
@ -114,6 +113,20 @@ public class AiModelController {
|
||||||
return ApiResponse.ok(aiModelService.testLocalConnectivity(dto.getBaseUrl(), dto.getApiKey()));
|
return ApiResponse.ok(aiModelService.testLocalConnectivity(dto.getBaseUrl(), dto.getApiKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "测试LLM模型连通性")
|
||||||
|
@PostMapping("/llm-connectivity-test")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> testLlmConnectivity(@RequestBody AiModelDTO dto) {
|
||||||
|
if ("custom".equalsIgnoreCase(dto.getProvider()) && (dto.getBaseUrl() == null || dto.getBaseUrl().isBlank())) {
|
||||||
|
return ApiResponse.error("Base URL不能为空");
|
||||||
|
}
|
||||||
|
if (dto.getModelCode() == null || dto.getModelCode().isBlank()) {
|
||||||
|
return ApiResponse.error("模型名称不能为空");
|
||||||
|
}
|
||||||
|
aiModelService.testLlmConnectivity(dto);
|
||||||
|
return ApiResponse.ok(Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取默认AI模型")
|
@Operation(summary = "获取默认AI模型")
|
||||||
@GetMapping("/default")
|
@GetMapping("/default")
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.imeeting.dto.android.legacy;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LegacyScreenSaverCatalogResponse {
|
||||||
|
|
||||||
|
@JsonProperty("refresh_interval_sec")
|
||||||
|
private Integer refreshIntervalSec;
|
||||||
|
|
||||||
|
@JsonProperty("play_mode")
|
||||||
|
private String playMode;
|
||||||
|
|
||||||
|
@JsonProperty("source_scope")
|
||||||
|
private String sourceScope;
|
||||||
|
|
||||||
|
@JsonProperty("display_duration_sec")
|
||||||
|
private Integer displayDurationSec;
|
||||||
|
|
||||||
|
private List<LegacyScreenSaverItemResponse> items;
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ public class AiModelDTO {
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
private String apiPath;
|
private String apiPath;
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
|
private String testMessage;
|
||||||
private String modelCode;
|
private String modelCode;
|
||||||
private String wsUrl;
|
private String wsUrl;
|
||||||
private BigDecimal temperature;
|
private BigDecimal temperature;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "屏保用户播放设置请求")
|
||||||
|
public class ScreenSaverUserSettingsDTO {
|
||||||
|
|
||||||
|
@Schema(description = "统一屏保展示时长(秒)", example = "15")
|
||||||
|
private Integer displayDurationSec;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "屏保用户播放设置响应")
|
||||||
|
public class ScreenSaverUserSettingsVO {
|
||||||
|
|
||||||
|
@Schema(description = "用户 ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "统一屏保展示时长(秒)")
|
||||||
|
private Integer displayDurationSec;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "屏保用户播放设置实体")
|
||||||
|
@TableName("biz_screen_saver_user_settings")
|
||||||
|
public class ScreenSaverUserSettings {
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
@Schema(description = "设置 ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
@Schema(description = "租户 ID")
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "用户 ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "统一屏保展示时长(秒)")
|
||||||
|
private Integer displayDurationSec;
|
||||||
|
|
||||||
|
@TableLogic(value = "0", delval = "1")
|
||||||
|
@Schema(description = "逻辑删除标记")
|
||||||
|
private Integer isDeleted;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.imeeting.mapper.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.imeeting.entity.biz.ScreenSaverUserSettings;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ScreenSaverUserSettingsMapper extends BaseMapper<ScreenSaverUserSettings> {
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ public interface AiModelService {
|
||||||
PageResult<List<AiModelVO>> pageModels(Integer current, Integer size, String name, String type, Long tenantId);
|
PageResult<List<AiModelVO>> pageModels(Integer current, Integer size, String name, String type, Long tenantId);
|
||||||
List<String> fetchRemoteModels(String provider, String baseUrl, String apiKey);
|
List<String> fetchRemoteModels(String provider, String baseUrl, String apiKey);
|
||||||
AiLocalProfileVO testLocalConnectivity(String baseUrl, String apiKey);
|
AiLocalProfileVO testLocalConnectivity(String baseUrl, String apiKey);
|
||||||
|
void testLlmConnectivity(AiModelDTO dto);
|
||||||
AiModelVO getDefaultModel(String type, Long tenantId);
|
AiModelVO getDefaultModel(String type, Long tenantId);
|
||||||
AiModelVO getModelById(Long id, String type);
|
AiModelVO getModelById(Long id, String type);
|
||||||
boolean removeModelById(Long id, String type);
|
boolean removeModelById(Long id, String type);
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -43,6 +44,20 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
|
|
||||||
private static final String TYPE_ASR = "ASR";
|
private static final String TYPE_ASR = "ASR";
|
||||||
private static final String TYPE_LLM = "LLM";
|
private static final String TYPE_LLM = "LLM";
|
||||||
|
private static final String DEFAULT_LLM_API_PATH = "/v1/chat/completions";
|
||||||
|
private static final String DEFAULT_ANTHROPIC_API_PATH = "/messages";
|
||||||
|
private static final String CONNECTIVITY_TEST_SYSTEM_PROMPT = """
|
||||||
|
You are an LLM connectivity test assistant.
|
||||||
|
You must return exactly one JSON object and nothing else.
|
||||||
|
Do not return markdown, code fences, explanations, or any extra text.
|
||||||
|
The JSON schema is fixed:
|
||||||
|
{"status":"success","message":"LLM connectivity test passed"}
|
||||||
|
Rules:
|
||||||
|
1. status must be success
|
||||||
|
2. message must be LLM connectivity test passed
|
||||||
|
3. no extra fields are allowed
|
||||||
|
""";
|
||||||
|
private static final String DEFAULT_LLM_TEST_MESSAGE = "Please reply with the fixed JSON payload.";
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final AsrModelMapper asrModelMapper;
|
private final AsrModelMapper asrModelMapper;
|
||||||
|
|
@ -50,6 +65,7 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
|
|
||||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(300))
|
.connectTimeout(Duration.ofSeconds(300))
|
||||||
|
.version(HttpClient.Version.HTTP_1_1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -161,14 +177,7 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
.timeout(Duration.ofSeconds(10))
|
.timeout(Duration.ofSeconds(10))
|
||||||
.GET();
|
.GET();
|
||||||
|
|
||||||
if ("anthropic".equals(providerKey)) {
|
applyProviderAuthHeaders(requestBuilder, providerKey, apiKey);
|
||||||
if (apiKey != null && !apiKey.isBlank()) {
|
|
||||||
requestBuilder.header("x-api-key", apiKey);
|
|
||||||
}
|
|
||||||
requestBuilder.header("anthropic-version", "2023-06-01");
|
|
||||||
} else if (!"gemini".equals(providerKey) && apiKey != null && !apiKey.isBlank()) {
|
|
||||||
requestBuilder.header("Authorization", "Bearer " + apiKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
|
|
@ -227,6 +236,138 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
return fetchLocalProfile(baseUrl, apiKey);
|
return fetchLocalProfile(baseUrl, apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void testLlmConnectivity(AiModelDTO dto) {
|
||||||
|
String providerKey = normalizeProvider(dto.getProvider());
|
||||||
|
if ("anthropic".equals(providerKey)) {
|
||||||
|
testAnthropicConnectivity(dto, providerKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("gemini".equals(providerKey) || "google".equals(providerKey)) {
|
||||||
|
testGeminiConnectivity(dto, providerKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
testOpenAiCompatibleConnectivity(dto, providerKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testOpenAiCompatibleConnectivity(AiModelDTO dto, String providerKey) {
|
||||||
|
String resolvedBaseUrl = resolveBaseUrl(providerKey, dto.getBaseUrl());
|
||||||
|
String targetUrl = appendPath(resolvedBaseUrl,
|
||||||
|
dto.getApiPath() == null || dto.getApiPath().isBlank() ? DEFAULT_LLM_API_PATH : dto.getApiPath());
|
||||||
|
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("model", dto.getModelCode().trim());
|
||||||
|
body.put("stream", false);
|
||||||
|
body.put("max_tokens", 32);
|
||||||
|
if (dto.getTemperature() != null) {
|
||||||
|
body.put("temperature", dto.getTemperature());
|
||||||
|
}
|
||||||
|
if (dto.getTopP() != null) {
|
||||||
|
body.put("top_p", dto.getTopP());
|
||||||
|
}
|
||||||
|
body.put("response_format", Map.of("type", "json_object"));
|
||||||
|
body.put("messages", buildConnectivityTestMessages(dto.getTestMessage()));
|
||||||
|
|
||||||
|
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(targetUrl))
|
||||||
|
.timeout(Duration.ofSeconds(20))
|
||||||
|
.header("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.header("Accept", "application/json");
|
||||||
|
applyProviderAuthHeaders(requestBuilder, providerKey, dto.getApiKey());
|
||||||
|
|
||||||
|
executeConnectivityRequest(targetUrl, dto.getModelCode(), requestBuilder, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testAnthropicConnectivity(AiModelDTO dto, String providerKey) {
|
||||||
|
String resolvedBaseUrl = resolveBaseUrl(providerKey, dto.getBaseUrl());
|
||||||
|
String targetUrl = appendPath(resolvedBaseUrl,
|
||||||
|
dto.getApiPath() == null || dto.getApiPath().isBlank() ? DEFAULT_ANTHROPIC_API_PATH : dto.getApiPath());
|
||||||
|
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("model", dto.getModelCode().trim());
|
||||||
|
body.put("system", CONNECTIVITY_TEST_SYSTEM_PROMPT);
|
||||||
|
body.put("max_tokens", 32);
|
||||||
|
body.put("messages", List.of(
|
||||||
|
Map.of("role", "user", "content", resolveTestMessage(dto.getTestMessage()))
|
||||||
|
));
|
||||||
|
|
||||||
|
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(targetUrl))
|
||||||
|
.timeout(Duration.ofSeconds(20))
|
||||||
|
.header("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.header("Accept", "application/json");
|
||||||
|
applyProviderAuthHeaders(requestBuilder, providerKey, dto.getApiKey());
|
||||||
|
|
||||||
|
executeConnectivityRequest(targetUrl, dto.getModelCode(), requestBuilder, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testGeminiConnectivity(AiModelDTO dto, String providerKey) {
|
||||||
|
String resolvedBaseUrl = resolveBaseUrl(providerKey, dto.getBaseUrl());
|
||||||
|
String apiPath = dto.getApiPath();
|
||||||
|
if (apiPath == null || apiPath.isBlank()) {
|
||||||
|
apiPath = "/models/" + dto.getModelCode().trim() + ":generateContent";
|
||||||
|
}
|
||||||
|
String targetUrl = appendPath(resolvedBaseUrl, apiPath);
|
||||||
|
if (dto.getApiKey() != null && !dto.getApiKey().isBlank()) {
|
||||||
|
targetUrl = appendQueryParam(targetUrl, "key", dto.getApiKey().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
|
body.put("systemInstruction", Map.of(
|
||||||
|
"parts", List.of(Map.of("text", CONNECTIVITY_TEST_SYSTEM_PROMPT))
|
||||||
|
));
|
||||||
|
body.put("contents", List.of(
|
||||||
|
Map.of(
|
||||||
|
"role", "user",
|
||||||
|
"parts", List.of(Map.of("text", resolveTestMessage(dto.getTestMessage())))
|
||||||
|
)
|
||||||
|
));
|
||||||
|
if (dto.getTemperature() != null || dto.getTopP() != null) {
|
||||||
|
Map<String, Object> generationConfig = new LinkedHashMap<>();
|
||||||
|
if (dto.getTemperature() != null) {
|
||||||
|
generationConfig.put("temperature", dto.getTemperature());
|
||||||
|
}
|
||||||
|
if (dto.getTopP() != null) {
|
||||||
|
generationConfig.put("topP", dto.getTopP());
|
||||||
|
}
|
||||||
|
body.put("generationConfig", generationConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(targetUrl))
|
||||||
|
.timeout(Duration.ofSeconds(20))
|
||||||
|
.header("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.header("Accept", "application/json");
|
||||||
|
|
||||||
|
executeConnectivityRequest(targetUrl, dto.getModelCode(), requestBuilder, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeConnectivityRequest(String targetUrl, String modelCode, HttpRequest.Builder requestBuilder, Object body) {
|
||||||
|
try {
|
||||||
|
String requestBody = objectMapper.writeValueAsString(body);
|
||||||
|
log.info("Testing LLM connectivity, url={}, model={}", targetUrl, modelCode);
|
||||||
|
HttpRequest request = requestBuilder
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
|
throw new RuntimeException("HTTP " + response.statusCode() + ", body=" + response.body());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode root = objectMapper.readTree(response.body());
|
||||||
|
String responseContent = readConnectivityResponseContent(root);
|
||||||
|
String resultMessage = extractConnectivityResultMessage(responseContent);
|
||||||
|
if (resultMessage == null || resultMessage.isBlank()) {
|
||||||
|
throw new RuntimeException("Unexpected empty connectivity response, body=" + response.body());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
String detail = describeException(e);
|
||||||
|
log.error("LLM connectivity test failed, url={}, detail={}", targetUrl, detail, e);
|
||||||
|
throw new RuntimeException("LLM连通性测试失败 url=" + targetUrl + ", detail=" + detail, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private AiLocalProfileVO fetchLocalProfile(String baseUrl, String apiKey) {
|
private AiLocalProfileVO fetchLocalProfile(String baseUrl, String apiKey) {
|
||||||
String targetUrl = appendPath(baseUrl, "api/v1/system/profile");
|
String targetUrl = appendPath(baseUrl, "api/v1/system/profile");
|
||||||
try {
|
try {
|
||||||
|
|
@ -297,10 +438,10 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
throw new RuntimeException("本地模型配置保存失败: HTTP " + response.statusCode());
|
throw new RuntimeException("闂佸搫鐗滈崜娆忥耿閺夋嚦鐔煎灳瀹曞洠鍋撻悜鑺ョ厐鐎广儱娲ㄩ弸鍌毲庨崶銊х畵闁宦板妽瀵板嫭娼忛銉? HTTP " + response.statusCode());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("本地模型配置保存失败: " + e.getMessage(), e);
|
throw new RuntimeException("闂佸搫鐗滈崜娆忥耿閺夋嚦鐔煎灳瀹曞洠鍋撻悜鑺ョ厐鐎广儱娲ㄩ弸鍌毲庨崶銊х畵闁宦板妽瀵板嫭娼忛銉? " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,7 +476,227 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String appendPath(String baseUrl, String path) {
|
private String appendPath(String baseUrl, String path) {
|
||||||
return baseUrl.endsWith("/") ? baseUrl + path : baseUrl + "/" + path;
|
if (baseUrl == null || baseUrl.isBlank()) {
|
||||||
|
throw new RuntimeException("baseUrl is required");
|
||||||
|
}
|
||||||
|
String trimmedBaseUrl = baseUrl.trim();
|
||||||
|
if (path == null || path.isBlank()) {
|
||||||
|
return trimmedBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmedPath = path.trim();
|
||||||
|
if (trimmedPath.startsWith("http://") || trimmedPath.startsWith("https://")) {
|
||||||
|
return trimmedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalizedBaseUrl = trimmedBaseUrl.endsWith("/")
|
||||||
|
? trimmedBaseUrl.substring(0, trimmedBaseUrl.length() - 1)
|
||||||
|
: trimmedBaseUrl;
|
||||||
|
if (!trimmedPath.startsWith("/")) {
|
||||||
|
return normalizedBaseUrl + "/" + trimmedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
URI baseUri = URI.create(normalizedBaseUrl + "/");
|
||||||
|
String basePath = baseUri.getPath();
|
||||||
|
if (basePath != null && !basePath.isBlank() && !"/".equals(basePath)) {
|
||||||
|
String normalizedBasePath = basePath.endsWith("/") ? basePath.substring(0, basePath.length() - 1) : basePath;
|
||||||
|
if (trimmedPath.startsWith(normalizedBasePath + "/")) {
|
||||||
|
return baseUri.resolve(trimmedPath).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalizedBaseUrl + trimmedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String appendQueryParam(String url, String name, String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
String delimiter = url.contains("?") ? "&" : "?";
|
||||||
|
return url + delimiter + name + "=" + URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyProviderAuthHeaders(HttpRequest.Builder requestBuilder, String providerKey, String apiKey) {
|
||||||
|
if ("anthropic".equals(providerKey)) {
|
||||||
|
if (apiKey != null && !apiKey.isBlank()) {
|
||||||
|
requestBuilder.header("x-api-key", apiKey.trim());
|
||||||
|
}
|
||||||
|
requestBuilder.header("anthropic-version", "2023-06-01");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("gemini".equals(providerKey) || "google".equals(providerKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (apiKey != null && !apiKey.isBlank()) {
|
||||||
|
requestBuilder.header("Authorization", buildAuthorization(apiKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildAuthorization(String apiKey) {
|
||||||
|
String trimmedApiKey = apiKey == null ? "" : apiKey.trim();
|
||||||
|
if (trimmedApiKey.isEmpty()) {
|
||||||
|
return trimmedApiKey;
|
||||||
|
}
|
||||||
|
return trimmedApiKey.startsWith("Bearer ") ? trimmedApiKey : "Bearer " + trimmedApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTestMessage(String testMessage) {
|
||||||
|
if (testMessage == null || testMessage.isBlank()) {
|
||||||
|
return DEFAULT_LLM_TEST_MESSAGE;
|
||||||
|
}
|
||||||
|
return testMessage.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, String>> buildConnectivityTestMessages(String testMessage) {
|
||||||
|
return List.of(
|
||||||
|
Map.of("role", "system", "content", CONNECTIVITY_TEST_SYSTEM_PROMPT),
|
||||||
|
Map.of("role", "user", "content", resolveTestMessage(testMessage))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readConnectivityResponseContent(JsonNode root) {
|
||||||
|
String anthropicText = root.path("content").path(0).path("text").asText(null);
|
||||||
|
if (anthropicText != null && !anthropicText.isBlank()) {
|
||||||
|
return anthropicText;
|
||||||
|
}
|
||||||
|
|
||||||
|
String chatCompletionContent = root.path("choices").path(0).path("message").path("content").asText(null);
|
||||||
|
if (chatCompletionContent != null && !chatCompletionContent.isBlank()) {
|
||||||
|
return chatCompletionContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
String textCompletionContent = root.path("choices").path(0).path("text").asText(null);
|
||||||
|
if (textCompletionContent != null && !textCompletionContent.isBlank()) {
|
||||||
|
return textCompletionContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
String responsesApiContent = root.path("output_text").asText(null);
|
||||||
|
if (responsesApiContent != null && !responsesApiContent.isBlank()) {
|
||||||
|
return responsesApiContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode candidatesNode = root.path("candidates");
|
||||||
|
if (candidatesNode.isArray()) {
|
||||||
|
for (JsonNode candidate : candidatesNode) {
|
||||||
|
JsonNode partsNode = candidate.path("content").path("parts");
|
||||||
|
if (!partsNode.isArray()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (JsonNode part : partsNode) {
|
||||||
|
String text = part.path("text").asText(null);
|
||||||
|
if (text != null && !text.isBlank()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode outputNode = root.path("output");
|
||||||
|
if (outputNode.isArray()) {
|
||||||
|
for (JsonNode item : outputNode) {
|
||||||
|
JsonNode contentArray = item.path("content");
|
||||||
|
if (!contentArray.isArray()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (JsonNode contentItem : contentArray) {
|
||||||
|
String text = contentItem.path("text").asText(null);
|
||||||
|
if (text != null && !text.isBlank()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractConnectivityResultMessage(String responseContent) {
|
||||||
|
if (responseContent == null || responseContent.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String trimmed = responseContent.trim();
|
||||||
|
JsonNode parsed = tryParseConnectivityPayload(trimmed);
|
||||||
|
if (parsed != null) {
|
||||||
|
String status = parsed.path("status").asText("");
|
||||||
|
String message = parsed.path("message").asText("");
|
||||||
|
if ("success".equalsIgnoreCase(status) && !message.isBlank()) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode tryParseConnectivityPayload(String text) {
|
||||||
|
try {
|
||||||
|
return objectMapper.readTree(text);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
String jsonObject = extractFirstJsonObject(text);
|
||||||
|
if (jsonObject == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readTree(jsonObject);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractFirstJsonObject(String text) {
|
||||||
|
if (text == null || text.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int start = text.indexOf('{');
|
||||||
|
if (start < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int depth = 0;
|
||||||
|
boolean inString = false;
|
||||||
|
boolean escaped = false;
|
||||||
|
for (int i = start; i < text.length(); i++) {
|
||||||
|
char ch = text.charAt(i);
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '\\') {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '"') {
|
||||||
|
inString = !inString;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inString) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == '{') {
|
||||||
|
depth++;
|
||||||
|
} else if (ch == '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0) {
|
||||||
|
return text.substring(start, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String describeException(Throwable throwable) {
|
||||||
|
if (throwable == null) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
String message = throwable.getMessage();
|
||||||
|
if (message != null && !message.isBlank()) {
|
||||||
|
return throwable.getClass().getSimpleName() + ": " + message;
|
||||||
|
}
|
||||||
|
Throwable cause = throwable.getCause();
|
||||||
|
if (cause != null && cause != throwable) {
|
||||||
|
String causeMessage = describeException(cause);
|
||||||
|
if (causeMessage != null && !causeMessage.isBlank()) {
|
||||||
|
return throwable.getClass().getSimpleName() + " <- " + causeMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return throwable.getClass().getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String sanitizeModelName(String rawName) {
|
private String sanitizeModelName(String rawName) {
|
||||||
|
|
@ -357,19 +718,19 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
|
|
||||||
private void validateModel(AiModelDTO dto) {
|
private void validateModel(AiModelDTO dto) {
|
||||||
if (dto == null) {
|
if (dto == null) {
|
||||||
throw new RuntimeException("模型配置不能为空");
|
throw new RuntimeException("妯″瀷閰嶇疆涓嶈兘涓虹┖");
|
||||||
}
|
}
|
||||||
if ("custom".equals(normalizeProvider(dto.getProvider()))) {
|
if ("custom".equals(normalizeProvider(dto.getProvider()))) {
|
||||||
if (dto.getApiKey() == null || dto.getApiKey().isBlank()) {
|
if (dto.getApiKey() == null || dto.getApiKey().isBlank()) {
|
||||||
throw new RuntimeException("本地模型必须填写API Key");
|
throw new RuntimeException("鏈湴妯″瀷蹇呴』濉啓API Key");
|
||||||
}
|
}
|
||||||
if (TYPE_ASR.equals(normalizeType(dto.getModelType()))) {
|
if (TYPE_ASR.equals(normalizeType(dto.getModelType()))) {
|
||||||
Map<String, Object> mediaConfig = dto.getMediaConfig() == null ? Collections.emptyMap() : dto.getMediaConfig();
|
Map<String, Object> mediaConfig = dto.getMediaConfig() == null ? Collections.emptyMap() : dto.getMediaConfig();
|
||||||
if (readConfigString(mediaConfig.get("speakerModel")) == null) {
|
if (readConfigString(mediaConfig.get("speakerModel")) == null) {
|
||||||
throw new RuntimeException("本地ASR模型必须选择声纹模型");
|
throw new RuntimeException("Local ASR model requires a speaker model");
|
||||||
}
|
}
|
||||||
if (mediaConfig.get("svThreshold") == null) {
|
if (mediaConfig.get("svThreshold") == null) {
|
||||||
throw new RuntimeException("本地ASR模型必须填写声纹阈值");
|
throw new RuntimeException("Local ASR model requires svThreshold");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.imeeting.dto.biz.AiModelDTO;
|
||||||
|
import com.imeeting.mapper.biz.AsrModelMapper;
|
||||||
|
import com.imeeting.mapper.biz.LlmModelMapper;
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
class AiModelServiceImplTest {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private HttpServer server;
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
if (server != null) {
|
||||||
|
server.stop(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLlmConnectivityShouldCallBaseUrlAndApiPathWithAuthorizationAndMessage() throws Exception {
|
||||||
|
AtomicReference<String> requestPath = new AtomicReference<>();
|
||||||
|
AtomicReference<String> authorization = new AtomicReference<>();
|
||||||
|
AtomicReference<String> body = new AtomicReference<>();
|
||||||
|
|
||||||
|
server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||||
|
server.createContext("/gateway/v1/chat/completions", exchange -> {
|
||||||
|
captureRequest(exchange, requestPath, authorization, body);
|
||||||
|
writeJson(exchange, 200, "{\"choices\":[{\"message\":{\"content\":\"{\\\"status\\\":\\\"success\\\",\\\"message\\\":\\\"LLM connectivity test passed\\\"}\"}}]}");
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||||
|
objectMapper,
|
||||||
|
mock(AsrModelMapper.class),
|
||||||
|
mock(LlmModelMapper.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
AiModelDTO dto = new AiModelDTO();
|
||||||
|
dto.setBaseUrl("http://127.0.0.1:" + server.getAddress().getPort() + "/gateway");
|
||||||
|
dto.setApiPath("/v1/chat/completions");
|
||||||
|
dto.setApiKey("test-key");
|
||||||
|
dto.setModelCode("gpt-test");
|
||||||
|
dto.setTestMessage("璇峰洖澶嶏細杩炴帴姝e父");
|
||||||
|
|
||||||
|
service.testLlmConnectivity(dto);
|
||||||
|
|
||||||
|
assertEquals("/gateway/v1/chat/completions", requestPath.get());
|
||||||
|
assertEquals("Bearer test-key", authorization.get());
|
||||||
|
|
||||||
|
JsonNode requestJson = objectMapper.readTree(body.get());
|
||||||
|
assertEquals("gpt-test", requestJson.path("model").asText());
|
||||||
|
assertEquals(
|
||||||
|
"{\"status\":\"success\",\"message\":\"LLM connectivity test passed\"}",
|
||||||
|
requestJson.path("messages").path(0).path("content").asText().lines()
|
||||||
|
.filter(line -> line.trim().startsWith("{"))
|
||||||
|
.findFirst()
|
||||||
|
.orElse("")
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
assertEquals("璇峰洖澶嶏細杩炴帴姝e父", requestJson.path("messages").path(1).path("content").asText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLlmConnectivityShouldAvoidDuplicatingSharedPathPrefix() throws Exception {
|
||||||
|
AtomicReference<String> requestPath = new AtomicReference<>();
|
||||||
|
|
||||||
|
server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||||
|
server.createContext("/v1/chat/completions", exchange -> {
|
||||||
|
captureRequest(exchange, requestPath, new AtomicReference<>(), new AtomicReference<>());
|
||||||
|
writeJson(exchange, 200, "{\"choices\":[{\"message\":{\"content\":\"{\\\"status\\\":\\\"success\\\",\\\"message\\\":\\\"LLM connectivity test passed\\\"}\"}}]}");
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||||
|
objectMapper,
|
||||||
|
mock(AsrModelMapper.class),
|
||||||
|
mock(LlmModelMapper.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
AiModelDTO dto = new AiModelDTO();
|
||||||
|
dto.setBaseUrl("http://127.0.0.1:" + server.getAddress().getPort() + "/v1");
|
||||||
|
dto.setApiPath("/v1/chat/completions");
|
||||||
|
dto.setModelCode("gpt-test");
|
||||||
|
|
||||||
|
service.testLlmConnectivity(dto);
|
||||||
|
|
||||||
|
assertEquals("/v1/chat/completions", requestPath.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLlmConnectivityShouldAcceptPlainTextResponseWhenModelIgnoresJsonFormat() throws Exception {
|
||||||
|
server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||||
|
server.createContext("/v1/chat/completions", exchange -> {
|
||||||
|
captureRequest(exchange, new AtomicReference<>(), new AtomicReference<>(), new AtomicReference<>());
|
||||||
|
writeJson(exchange, 200, "{\"choices\":[{\"message\":{\"content\":\"thought\\n* Input: \\\"请回复:LLM 连通性测试成功。\\\"\"}}]}");
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||||
|
objectMapper,
|
||||||
|
mock(AsrModelMapper.class),
|
||||||
|
mock(LlmModelMapper.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
AiModelDTO dto = new AiModelDTO();
|
||||||
|
dto.setBaseUrl("http://127.0.0.1:" + server.getAddress().getPort());
|
||||||
|
dto.setApiPath("/v1/chat/completions");
|
||||||
|
dto.setModelCode("gpt-test");
|
||||||
|
|
||||||
|
service.testLlmConnectivity(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLlmConnectivityShouldUseHttp11ClientForCompatibility() throws Exception {
|
||||||
|
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||||
|
objectMapper,
|
||||||
|
mock(AsrModelMapper.class),
|
||||||
|
mock(LlmModelMapper.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
Field httpClientField = AiModelServiceImpl.class.getDeclaredField("httpClient");
|
||||||
|
httpClientField.setAccessible(true);
|
||||||
|
HttpClient httpClient = (HttpClient) httpClientField.get(service);
|
||||||
|
|
||||||
|
assertEquals(HttpClient.Version.HTTP_1_1, httpClient.version());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void captureRequest(HttpExchange exchange,
|
||||||
|
AtomicReference<String> requestPath,
|
||||||
|
AtomicReference<String> authorization,
|
||||||
|
AtomicReference<String> body) throws IOException {
|
||||||
|
requestPath.set(exchange.getRequestURI().getPath());
|
||||||
|
authorization.set(exchange.getRequestHeaders().getFirst("Authorization"));
|
||||||
|
body.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeJson(HttpExchange exchange, int status, String responseBody) throws IOException {
|
||||||
|
byte[] bytes = responseBody.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.getResponseHeaders().add("Content-Type", "application/json");
|
||||||
|
exchange.sendResponseHeaders(status, bytes.length);
|
||||||
|
try (OutputStream os = exchange.getResponseBody()) {
|
||||||
|
os.write(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ export interface AiModelDTO {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiPath?: string;
|
apiPath?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
testMessage?: string;
|
||||||
modelCode?: string;
|
modelCode?: string;
|
||||||
wsUrl?: string;
|
wsUrl?: string;
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
|
|
@ -100,6 +101,22 @@ export const testLocalModelConnectivity = (data: { baseUrl: string; apiKey: stri
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const testLlmModelConnectivity = (data: {
|
||||||
|
provider?: string;
|
||||||
|
baseUrl: string;
|
||||||
|
apiPath?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
modelCode: string;
|
||||||
|
temperature?: number;
|
||||||
|
topP?: number;
|
||||||
|
testMessage: string;
|
||||||
|
}) => {
|
||||||
|
return http.post<{ code: string; data: boolean; msg: string }>(
|
||||||
|
"/api/biz/aimodel/llm-connectivity-test",
|
||||||
|
data
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getAiModelDefault = (type: 'ASR' | 'LLM') => {
|
export const getAiModelDefault = (type: 'ASR' | 'LLM') => {
|
||||||
return http.get<{ code: string; data: AiModelVO; msg: string }>(
|
return http.get<{ code: string; data: AiModelVO; msg: string }>(
|
||||||
"/api/biz/aimodel/default",
|
"/api/biz/aimodel/default",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
getAiModelPage,
|
getAiModelPage,
|
||||||
getRemoteModelList,
|
getRemoteModelList,
|
||||||
saveAiModel,
|
saveAiModel,
|
||||||
|
testLlmModelConnectivity,
|
||||||
testLocalModelConnectivity,
|
testLocalModelConnectivity,
|
||||||
updateAiModel,
|
updateAiModel,
|
||||||
} from "../../api/business/aimodel";
|
} from "../../api/business/aimodel";
|
||||||
|
|
@ -40,6 +41,8 @@ const PROVIDER_BASE_URL_MAP: Record<string, string> = {
|
||||||
groq: "https://api.groq.com/openai/v1",
|
groq: "https://api.groq.com/openai/v1",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_LLM_TEST_MESSAGE = "请回复:LLM 连通性测试成功。";
|
||||||
|
|
||||||
const AiModels: React.FC = () => {
|
const AiModels: React.FC = () => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
@ -277,6 +280,28 @@ const AiModels: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTestConnectivity = async () => {
|
const handleTestConnectivity = async () => {
|
||||||
|
if (activeType === "LLM") {
|
||||||
|
const values = await form.validateFields(["provider", "baseUrl", "apiPath", "modelCode"]);
|
||||||
|
const extraValues = form.getFieldsValue(["apiKey", "temperature", "topP"]);
|
||||||
|
setConnectivityLoading(true);
|
||||||
|
try {
|
||||||
|
await testLlmModelConnectivity({
|
||||||
|
provider: values.provider,
|
||||||
|
baseUrl: values.baseUrl,
|
||||||
|
apiPath: values.apiPath,
|
||||||
|
apiKey: extraValues.apiKey,
|
||||||
|
modelCode: values.modelCode,
|
||||||
|
temperature: extraValues.temperature,
|
||||||
|
topP: extraValues.topP,
|
||||||
|
testMessage: DEFAULT_LLM_TEST_MESSAGE,
|
||||||
|
});
|
||||||
|
message.success("LLM 连通性测试成功");
|
||||||
|
} finally {
|
||||||
|
setConnectivityLoading(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const values = await form.validateFields(["provider", "baseUrl", "apiKey"]);
|
const values = await form.validateFields(["provider", "baseUrl", "apiKey"]);
|
||||||
if (String(values.provider || "").toLowerCase() !== "custom") {
|
if (String(values.provider || "").toLowerCase() !== "custom") {
|
||||||
message.warning("仅本地模型支持连通性测试");
|
message.warning("仅本地模型支持连通性测试");
|
||||||
|
|
@ -486,7 +511,7 @@ const AiModels: React.FC = () => {
|
||||||
<Input.Password />
|
<Input.Password />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{isLocalProvider && (
|
{(activeType === "LLM" || isLocalProvider) && (
|
||||||
<Form.Item label="连通性测试">
|
<Form.Item label="连通性测试">
|
||||||
<Button icon={<WifiOutlined />} loading={connectivityLoading} onClick={handleTestConnectivity}>
|
<Button icon={<WifiOutlined />} loading={connectivityLoading} onClick={handleTestConnectivity}>
|
||||||
测试连接
|
测试连接
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue