Moonshot AI entfernt, Konfiguration auf Umgebungsvariablen umgestellt, Mail-Verschlüsselung auf SSL (Port 465)

- Moonshot AI Provider komplett entfernt (LlmConfig, LlmRestClient, application.properties)
- Mail-Konfiguration (Host, Port, User, Passwort, SMTP-Auth, SSL) auf Umgebungsvariablen umgestellt
- LM Studio Modell auf Umgebungsvariable LMSTUDIO_MODEL umgestellt
- Mail-Verschlüsselung von STARTTLS auf SSL umgestellt (Port 465)
- Version auf 0.9.9 erhöht

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 13:37:42 +01:00
parent 7ba2880148
commit 22fdc13cba
5 changed files with 42 additions and 97 deletions

View File

@@ -2,5 +2,5 @@
echo "G8m0T3vz" | docker login registry.assecutor.org -u adsg --password-stdin echo "G8m0T3vz" | docker login registry.assecutor.org -u adsg --password-stdin
# Dann ganz normal pushen # Dann ganz normal pushen
docker buildx build --platform linux/amd64 -t registry.assecutor.org/votianlt:0.9.6 --push . docker buildx build --platform linux/amd64 -t registry.assecutor.org/votianlt:0.9.9 --push .

View File

@@ -6,7 +6,7 @@
<groupId>de.assecutor.votianlt</groupId> <groupId>de.assecutor.votianlt</groupId>
<artifactId>votianlt</artifactId> <artifactId>votianlt</artifactId>
<version>0.9.6</version> <version>0.9.9</version>
<packaging>jar</packaging> <packaging>jar</packaging>

View File

@@ -11,17 +11,13 @@ import java.net.URL;
import java.util.Base64; import java.util.Base64;
/** /**
* Configuration for LLM integration. Supports LM Studio and Moonshot AI. Switch * Configuration for LLM integration via LM Studio. The LM Studio instance
* provider via {@code app.ai.provider=lmstudio|moonshot} in * exposes an OpenAI-compatible API at {@code /v1/chat/completions}.
* application.properties.
*/ */
@Configuration @Configuration
@Slf4j @Slf4j
public class LlmConfig { public class LlmConfig {
@Value("${app.ai.provider:lmstudio}")
private String provider;
@Value("${app.ai.lmstudio.base-url}") @Value("${app.ai.lmstudio.base-url}")
private String lmstudioBaseUrl; private String lmstudioBaseUrl;
@@ -34,45 +30,29 @@ public class LlmConfig {
@Value("${app.ai.lmstudio.htaccess-password}") @Value("${app.ai.lmstudio.htaccess-password}")
private String lmstudioHtaccessPassword; private String lmstudioHtaccessPassword;
@Value("${app.ai.moonshot.base-url:https://api.moonshot.ai}")
private String moonshotBaseUrl;
@Value("${app.ai.moonshot.api-key:}")
private String moonshotApiKey;
@Value("${app.ai.moonshot.model:moonshot-v1-8k}")
private String moonshotModel;
@PostConstruct @PostConstruct
public void logConfig() { public void logConfig() {
log.info("=== LLM Configuration ==="); log.info("=== LLM Configuration ===");
log.info("Provider: {}", provider); log.info("Provider: lmstudio");
if ("moonshot".equalsIgnoreCase(provider)) { log.info("Base URL: {}", lmstudioBaseUrl);
log.info("Base URL: {}", moonshotBaseUrl); log.info("Model: {}", lmstudioModel);
log.info("Model: {}", moonshotModel); log.info("HTACCESS auth: {}", hasHtaccessCredentials() ? "configured" : "not configured");
log.info("API Key: {}***", moonshotApiKey.length() > 8 ? moonshotApiKey.substring(0, 8) : "***"); testConnection(lmstudioBaseUrl, lmstudioModel);
testConnection(moonshotBaseUrl, moonshotModel, moonshotApiKey);
} else {
log.info("Base URL: {}", lmstudioBaseUrl);
log.info("Model: {}", lmstudioModel);
log.info("HTACCESS auth: {}", hasHtaccessCredentials() ? "configured" : "not configured");
testConnection(lmstudioBaseUrl, lmstudioModel, null);
}
} }
private void testConnection(String baseUrl, String model, String apiKey) { private void testConnection(String baseUrl, String model) {
log.info("Testing LLM connection to: {}", baseUrl); log.info("Testing LLM connection to: {}", baseUrl);
// Test 1: Models endpoint // Test 1: Models endpoint
testEndpoint(baseUrl + "/v1/models", "GET", null, apiKey); testEndpoint(baseUrl + "/v1/models", "GET", null);
// Test 2: Chat completions (no streaming) // Test 2: Chat completions (no streaming)
String testPayload = "{\"model\":\"" + model String testPayload = "{\"model\":\"" + model
+ "\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"stream\":false}"; + "\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"stream\":false}";
testEndpoint(baseUrl + "/v1/chat/completions", "POST", testPayload, apiKey); testEndpoint(baseUrl + "/v1/chat/completions", "POST", testPayload);
} }
private void testEndpoint(String endpoint, String method, String payload, String apiKey) { private void testEndpoint(String endpoint, String method, String payload) {
try { try {
log.info("Testing endpoint: {} {}", method, endpoint); log.info("Testing endpoint: {} {}", method, endpoint);
URL url = URI.create(endpoint).toURL(); URL url = URI.create(endpoint).toURL();
@@ -85,8 +65,6 @@ public class LlmConfig {
String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword; String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword;
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes()); String encoded = Base64.getEncoder().encodeToString(credentials.getBytes());
connection.setRequestProperty("Authorization", "Basic " + encoded); connection.setRequestProperty("Authorization", "Basic " + encoded);
} else if (apiKey != null && !apiKey.isBlank()) {
connection.setRequestProperty("Authorization", "Bearer " + apiKey);
} }
if (payload != null) { if (payload != null) {
@@ -123,16 +101,12 @@ public class LlmConfig {
} }
} }
public String getProvider() {
return provider;
}
public String getBaseUrl() { public String getBaseUrl() {
return "moonshot".equalsIgnoreCase(provider) ? moonshotBaseUrl : lmstudioBaseUrl; return lmstudioBaseUrl;
} }
public String getModel() { public String getModel() {
return "moonshot".equalsIgnoreCase(provider) ? moonshotModel : lmstudioModel; return lmstudioModel;
} }
public boolean hasHtaccessCredentials() { public boolean hasHtaccessCredentials() {

View File

@@ -16,9 +16,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* Direct REST client for LLM APIs (LM Studio or Moonshot AI). Provider is * Direct REST client for LM Studio LLM API. Communicates via the
* selected via {@code app.ai.provider} in application.properties. Both * OpenAI-compatible /v1/chat/completions endpoint.
* providers expose an OpenAI-compatible /v1/chat/completions endpoint.
*/ */
@Component @Component
@Slf4j @Slf4j
@@ -27,45 +26,30 @@ public class LlmRestClient {
private final WebClient webClient; private final WebClient webClient;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final String model; private final String model;
private final String provider;
public LlmRestClient(@Value("${app.ai.provider}") String provider, public LlmRestClient(@Value("${app.ai.lmstudio.base-url}") String lmstudioBaseUrl,
@Value("${app.ai.lmstudio.base-url}") String lmstudioBaseUrl,
@Value("${app.ai.lmstudio.model}") String lmstudioModel, @Value("${app.ai.lmstudio.model}") String lmstudioModel,
@Value("${app.ai.lmstudio.htaccess-username}") String lmstudioHtaccessUsername, @Value("${app.ai.lmstudio.htaccess-username}") String lmstudioHtaccessUsername,
@Value("${app.ai.lmstudio.htaccess-password}") String lmstudioHtaccessPassword, @Value("${app.ai.lmstudio.htaccess-password}") String lmstudioHtaccessPassword,
@Value("${app.ai.moonshot.base-url}") String moonshotBaseUrl, ObjectMapper objectMapper) {
@Value("${app.ai.moonshot.api-key}") String moonshotApiKey,
@Value("${app.ai.moonshot.model}") String moonshotModel, ObjectMapper objectMapper) {
this.provider = provider.trim().toLowerCase(); this.model = lmstudioModel;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
WebClient.Builder builder = WebClient.builder(); WebClient.Builder builder = WebClient.builder();
builder.baseUrl(lmstudioBaseUrl + "/v1/chat/completions");
if ("moonshot".equals(this.provider)) { if (lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank()
this.model = moonshotModel; && lmstudioHtaccessPassword != null && !lmstudioHtaccessPassword.isBlank()) {
builder.baseUrl(moonshotBaseUrl + "/v1/chat/completions"); String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword;
if (moonshotApiKey != null && !moonshotApiKey.isBlank()) { String encoded = Base64.getEncoder()
builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + moonshotApiKey); .encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
} builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + encoded);
log.info("LlmRestClient initialized - Provider: moonshot, URL: {}/v1/chat/completions, Model: {}", log.info("LlmRestClient initialized (with HTACCESS auth) - URL: {}/v1/chat/completions, Model: {}",
moonshotBaseUrl, moonshotModel); lmstudioBaseUrl, lmstudioModel);
} else { } else {
this.model = lmstudioModel; log.info("LlmRestClient initialized - URL: {}/v1/chat/completions, Model: {}",
builder.baseUrl(lmstudioBaseUrl + "/v1/chat/completions"); lmstudioBaseUrl, lmstudioModel);
if (lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank()
&& lmstudioHtaccessPassword != null && !lmstudioHtaccessPassword.isBlank()) {
String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword;
String encoded = Base64.getEncoder()
.encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + encoded);
log.info("LlmRestClient initialized - Provider: lmstudio (with HTACCESS auth), "
+ "URL: {}/v1/chat/completions, Model: {}", lmstudioBaseUrl, lmstudioModel);
} else {
log.info("LlmRestClient initialized - Provider: lmstudio, URL: {}/v1/chat/completions, Model: {}",
lmstudioBaseUrl, lmstudioModel);
}
} }
this.webClient = builder.build(); this.webClient = builder.build();
@@ -104,7 +88,7 @@ public class LlmRestClient {
Map.of("role", "user", "content", userMessage)), Map.of("role", "user", "content", userMessage)),
"temperature", temperature, "max_tokens", maxTokens, "stream", false); "temperature", temperature, "max_tokens", maxTokens, "stream", false);
log.info("Sending request to LLM [{}] (model: {}, prompt length: {} chars)...", provider, model, log.info("Sending request to LLM (model: {}, prompt length: {} chars)...", model,
userMessage.length()); userMessage.length());
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@@ -118,7 +102,7 @@ public class LlmRestClient {
return extractContent(response); return extractContent(response);
} catch (Exception e) { } catch (Exception e) {
log.error("Error calling LLM API [{}]: {} - {}", provider, e.getClass().getSimpleName(), e.getMessage()); log.error("Error calling LLM API: {} - {}", e.getClass().getSimpleName(), e.getMessage());
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("Full stack trace:", e); log.debug("Full stack trace:", e);
} }
@@ -133,10 +117,6 @@ public class LlmRestClient {
return chat(null, userMessage); return chat(null, userMessage);
} }
public String getProvider() {
return provider;
}
public String getModel() { public String getModel() {
return model; return model;
} }

View File

@@ -38,12 +38,12 @@ spring.data.mongodb.connect-timeout=10000
spring.data.mongodb.server-selection-timeout=5000 spring.data.mongodb.server-selection-timeout=5000
# Mail Configuration (Spring Boot Standard) # Mail Configuration (Spring Boot Standard)
spring.mail.host=mailhub.assecutor.org spring.mail.host=${MAIL_HOST}
spring.mail.port=587 spring.mail.port=${MAIL_PORT}
spring.mail.username=noreply@assecutor.org spring.mail.username=${MAIL_USERNAME}
spring.mail.password=OStRIL,_,31 spring.mail.password=${MAIL_PASSWORD}
spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.auth=${MAIL_SMTP_AUTH}
spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.ssl.enable=${MAIL_SMTP_SSL}
# HTTP request size limits for large payloads # HTTP request size limits for large payloads
server.max-http-request-header-size=8MB server.max-http-request-header-size=8MB
@@ -73,26 +73,17 @@ app.version=@project.version@
app.google.maps.api-key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE app.google.maps.api-key=AIzaSyDnbitL06iLp3elmj-WtPudCykX9xvXcVE
# =========================================== # ===========================================
# LLM Configuration # LLM Configuration (LM Studio)
# Provider: lmstudio | moonshot
# =========================================== # ===========================================
app.ai.provider=moonshot
# --- LM Studio ---
app.ai.lmstudio.base-url=${LMSTUDIO_URL} app.ai.lmstudio.base-url=${LMSTUDIO_URL}
app.ai.lmstudio.model=local-model app.ai.lmstudio.model=${LMSTUDIO_MODEL}
app.ai.lmstudio.htaccess-username=${LMSTUDIO_HTACCESS_USERNAME} app.ai.lmstudio.htaccess-username=${LMSTUDIO_HTACCESS_USERNAME}
app.ai.lmstudio.htaccess-password=${LMSTUDIO_HTACCESS_PASSWORD} app.ai.lmstudio.htaccess-password=${LMSTUDIO_HTACCESS_PASSWORD}
# --- Moonshot AI (kimi) ---
app.ai.moonshot.base-url=https://api.moonshot.ai
app.ai.moonshot.api-key=sk-EfHJfwCsxiZbOoBJ21OLWb9RUJQXSXAFIFGKnOedKke5JYZp
app.ai.moonshot.model=moonshot-v1-8k
# Spring AI OpenAI properties (Pflicht für Auto-Configuration, werden vom LlmRestClient überschrieben) # Spring AI OpenAI properties (Pflicht für Auto-Configuration, werden vom LlmRestClient überschrieben)
spring.ai.openai.base-url=${LMSTUDIO_URL} spring.ai.openai.base-url=${LMSTUDIO_URL}
spring.ai.openai.api-key=not-used spring.ai.openai.api-key=not-used
spring.ai.openai.chat.options.model=local-model spring.ai.openai.chat.options.model=${LMSTUDIO_MODEL}
spring.ai.openai.chat.options.temperature=0.7 spring.ai.openai.chat.options.temperature=0.7
spring.ai.openai.chat.options.stream=false spring.ai.openai.chat.options.stream=false
spring.ai.openai.connect-timeout=10s spring.ai.openai.connect-timeout=10s