feat: support deepseek and openai-compatible providers
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import com.svnlog.web.model.TaskInfo;
|
||||
import com.svnlog.web.model.TaskStatus;
|
||||
|
||||
import okio.Buffer;
|
||||
|
||||
class AiWorkflowServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldResolveDeepSeekProviderByDefault() {
|
||||
final AiWorkflowService service = new AiWorkflowService(
|
||||
buildOutputFileService(),
|
||||
new SettingsService(buildOutputFileService(), new SvnPresetService()),
|
||||
new AiInputValidator()
|
||||
);
|
||||
|
||||
final AiWorkflowService.AiProviderContext context = service.resolveProviderContext(null);
|
||||
|
||||
Assertions.assertEquals(SettingsService.PROVIDER_DEEPSEEK, context.getProvider());
|
||||
Assertions.assertEquals("deepseek-chat", context.getStageOneModel());
|
||||
Assertions.assertEquals("deepseek-reasoner", context.getStageTwoModel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveOpenAiCompatibleModelsAndUrl() {
|
||||
final OutputFileService outputFileService = buildOutputFileService();
|
||||
final SettingsService settingsService = new SettingsService(outputFileService, new SvnPresetService());
|
||||
settingsService.updateSettings(
|
||||
null,
|
||||
SettingsService.PROVIDER_OPENAI_COMPATIBLE,
|
||||
"http://127.0.0.1:5001/v1/",
|
||||
"sk-openai-test",
|
||||
"deepseek-v4-flash",
|
||||
"deepseek-v4-pro",
|
||||
null,
|
||||
null
|
||||
);
|
||||
final AiWorkflowService service = new AiWorkflowService(outputFileService, settingsService, new AiInputValidator());
|
||||
|
||||
final AiWorkflowService.AiProviderContext context = service.resolveProviderContext(null);
|
||||
|
||||
Assertions.assertEquals(SettingsService.PROVIDER_OPENAI_COMPATIBLE, context.getProvider());
|
||||
Assertions.assertEquals("http://127.0.0.1:5001/v1/chat/completions", context.getApiUrl());
|
||||
Assertions.assertEquals("deepseek-v4-flash", context.getStageOneModel());
|
||||
Assertions.assertEquals("deepseek-v4-pro", context.getStageTwoModel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailFastWhenOpenAiCompatibleBaseUrlMissing() {
|
||||
final AiWorkflowService service = new AiWorkflowService(
|
||||
buildOutputFileService(),
|
||||
new StubSettingsService(buildOutputFileService(), " ", "sk-openai-test"),
|
||||
new AiInputValidator()
|
||||
);
|
||||
|
||||
final IllegalStateException error = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> service.resolveProviderContext(null)
|
||||
);
|
||||
|
||||
Assertions.assertTrue(error.getMessage().contains("OpenAI兼容 Base URL"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseCompatibleStreamWhenOnlyContentIsReturned() throws Exception {
|
||||
final OutputFileService outputFileService = buildOutputFileService();
|
||||
final SettingsService settingsService = new SettingsService(outputFileService, new SvnPresetService());
|
||||
settingsService.updateSettings(
|
||||
null,
|
||||
SettingsService.PROVIDER_OPENAI_COMPATIBLE,
|
||||
"http://127.0.0.1:5001/v1",
|
||||
"sk-openai-test",
|
||||
"deepseek-v4-flash",
|
||||
"deepseek-v4-pro",
|
||||
null,
|
||||
null
|
||||
);
|
||||
final AiWorkflowService service = new AiWorkflowService(outputFileService, settingsService, new AiInputValidator());
|
||||
final AiWorkflowService.AiProviderContext providerContext = service.resolveProviderContext(null);
|
||||
final TaskContext taskContext = new TaskContext(buildTaskInfo(), null, null);
|
||||
final Buffer buffer = new Buffer()
|
||||
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"{\\\"items\\\":[\"}}]}\n")
|
||||
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"{\\\"summary\\\":\\\"done\\\"}\"}}]}\n")
|
||||
.writeUtf8("data: {\"choices\":[{\"delta\":{\"content\":\"]}\"},\"finish_reason\":\"stop\"}]}\n")
|
||||
.writeUtf8("data: [DONE]\n");
|
||||
|
||||
final AiWorkflowService.AiStreamResult result = service.readStreamingResponse(
|
||||
buffer,
|
||||
taskContext,
|
||||
providerContext,
|
||||
"deepseek-v4-flash",
|
||||
8000
|
||||
);
|
||||
|
||||
Assertions.assertEquals("{\"items\":[{\"summary\":\"done\"}]}", result.getAnswer());
|
||||
Assertions.assertEquals("stop", result.getFinishReason());
|
||||
}
|
||||
|
||||
private OutputFileService buildOutputFileService() {
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
||||
return outputFileService;
|
||||
}
|
||||
|
||||
private TaskInfo buildTaskInfo() {
|
||||
final TaskInfo taskInfo = new TaskInfo();
|
||||
taskInfo.setTaskId("task-1");
|
||||
taskInfo.setStatus(TaskStatus.RUNNING);
|
||||
taskInfo.setCreatedAt(Instant.now());
|
||||
taskInfo.setUpdatedAt(Instant.now());
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
private static final class StubSettingsService extends SettingsService {
|
||||
private final String openaiBaseUrl;
|
||||
private final String openaiApiKey;
|
||||
|
||||
private StubSettingsService(OutputFileService outputFileService, String openaiBaseUrl, String openaiApiKey) {
|
||||
super(outputFileService, new SvnPresetService());
|
||||
this.openaiBaseUrl = openaiBaseUrl;
|
||||
this.openaiApiKey = openaiApiKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProvider() {
|
||||
return SettingsService.PROVIDER_OPENAI_COMPATIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOpenaiBaseUrl() {
|
||||
return openaiBaseUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOpenaiApiKey() {
|
||||
return openaiApiKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,40 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
public class HealthServiceTest {
|
||||
class HealthServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private TaskService taskService;
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (taskService != null) {
|
||||
taskService.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnDetailedHealthWhenDependenciesAvailable() throws Exception {
|
||||
OutputFileService outputFileService = Mockito.mock(OutputFileService.class);
|
||||
SettingsService settingsService = Mockito.mock(SettingsService.class);
|
||||
TaskService taskService = Mockito.mock(TaskService.class);
|
||||
void shouldReturnDetailedHealthAfterSettingsExpansion() throws Exception {
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
||||
final SettingsService settingsService = new SettingsService(outputFileService, new SvnPresetService());
|
||||
taskService = new TaskService(new TaskPersistenceService(), outputFileService);
|
||||
final HealthService healthService = new HealthService(outputFileService, settingsService, taskService);
|
||||
|
||||
Path outputDir = Files.createTempDirectory("health-service-test");
|
||||
Mockito.when(outputFileService.getOutputRoot()).thenReturn(outputDir);
|
||||
|
||||
Map<String, Object> settings = new HashMap<String, Object>();
|
||||
settings.put("apiKeyConfigured", true);
|
||||
Mockito.when(settingsService.getSettings()).thenReturn(settings);
|
||||
Mockito.when(taskService.getTasks()).thenReturn(new java.util.ArrayList<>());
|
||||
|
||||
HealthService healthService = new HealthService(outputFileService, settingsService, taskService);
|
||||
Map<String, Object> details = healthService.detailedHealth();
|
||||
final Map<String, Object> details = healthService.detailedHealth();
|
||||
|
||||
Assertions.assertEquals("ok", details.get("status"));
|
||||
Assertions.assertEquals(true, details.get("outputDirWritable"));
|
||||
Assertions.assertEquals(true, details.get("apiKeyConfigured"));
|
||||
Assertions.assertEquals(0, details.get("taskTotal"));
|
||||
Assertions.assertTrue(Boolean.TRUE.equals(details.get("outputDirWritable")));
|
||||
Assertions.assertTrue(details.containsKey("apiKeyConfigured"));
|
||||
Assertions.assertEquals(Integer.valueOf(0), details.get("taskTotal"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.svnlog.web.service;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class SettingsServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldReturnDefaultProviderAndOpenAiDefaults() throws Exception {
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
||||
final SettingsService settingsService = new SettingsService(outputFileService, new SvnPresetService());
|
||||
|
||||
final Map<String, Object> settings = settingsService.getSettings();
|
||||
|
||||
Assertions.assertEquals(SettingsService.PROVIDER_DEEPSEEK, settings.get("provider"));
|
||||
Assertions.assertEquals("http://127.0.0.1:5001/v1", settings.get("openaiBaseUrl"));
|
||||
Assertions.assertEquals("sk-f8b3f43e1fdd4f50b287050f08a6c7ed", settings.get("openaiApiKey"));
|
||||
Assertions.assertEquals("deepseek-v4-flash", settings.get("openaiStageOneModel"));
|
||||
Assertions.assertEquals("deepseek-v4-pro", settings.get("openaiStageTwoModel"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateProviderAndOpenAiSettings() throws Exception {
|
||||
final OutputFileService outputFileService = new OutputFileService();
|
||||
outputFileService.setOutputRoot(tempDir.resolve("outputs").toString());
|
||||
final SettingsService settingsService = new SettingsService(outputFileService, new SvnPresetService());
|
||||
final String newOutputDir = tempDir.resolve("custom-output").toString();
|
||||
|
||||
settingsService.updateSettings(
|
||||
"sk-deepseek-runtime",
|
||||
SettingsService.PROVIDER_OPENAI_COMPATIBLE,
|
||||
"http://localhost:5001/v1/",
|
||||
"sk-openai-runtime",
|
||||
"deepseek-v4-flash",
|
||||
"deepseek-v4-pro",
|
||||
newOutputDir,
|
||||
"preset-2"
|
||||
);
|
||||
|
||||
final Map<String, Object> settings = settingsService.getSettings();
|
||||
|
||||
Assertions.assertEquals(SettingsService.PROVIDER_OPENAI_COMPATIBLE, settings.get("provider"));
|
||||
Assertions.assertEquals("http://localhost:5001/v1/", settings.get("openaiBaseUrl"));
|
||||
Assertions.assertEquals("sk-openai-runtime", settings.get("openaiApiKey"));
|
||||
Assertions.assertEquals("deepseek-v4-flash", settings.get("openaiStageOneModel"));
|
||||
Assertions.assertEquals("deepseek-v4-pro", settings.get("openaiStageTwoModel"));
|
||||
Assertions.assertEquals("preset-2", settings.get("defaultSvnPresetId"));
|
||||
Assertions.assertEquals(Paths.get(newOutputDir).toAbsolutePath().normalize().toString(), settings.get("outputDir"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user