Kosten Endpunkt

This commit is contained in:
2026-06-01 14:21:42 +02:00
parent fb185c1a71
commit f3ae6859bf
21 changed files with 632 additions and 78 deletions

View File

@@ -73,6 +73,11 @@
<artifactId>langchain4j-ollama</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mistral-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- ================================================================ -->
<!-- Dokumentenverarbeitung -->

View File

@@ -1,6 +1,7 @@
package de.frigosped.dc.ai;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.mistralai.MistralAiChatModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
@@ -10,29 +11,42 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.time.Duration;
import java.util.Map;
/**
* CDI-Producer für die zwei KI-Modelle.
*
* Beide Modelle sprechen denselben Ollama-kompatiblen Endpunkt an,
* unterscheiden sich aber in Modellname und Timeout.
* Der X-API-KEY-Header wird über customHeaders gesetzt.
*
* Einsatz:
* @Inject @Named("main") ChatLanguageModel mainModel → Auswertung + Übersetzung
* @Inject @Named("ocr") ChatLanguageModel ocrModel → PDF-OCR (Vision)
*/
@ApplicationScoped
public class AiModelProducer {
@ConfigProperty(name = "dc.ai.base-url")
String baseUrl;
@ConfigProperty(name = "dc.ai.provider", defaultValue = "ollama")
String provider;
@ConfigProperty(name = "dc.ai.api-key")
String apiKey;
// --- Ollama ---
@ConfigProperty(name = "dc.ai.ollama.base-url", defaultValue = "")
String ollamaBaseUrl;
@ConfigProperty(name = "dc.ai.main.model")
String mainModelName;
@ConfigProperty(name = "dc.ai.ollama.api-key", defaultValue = "")
String ollamaApiKey;
@ConfigProperty(name = "dc.ai.ollama.main.model", defaultValue = "")
String ollamaMainModel;
@ConfigProperty(name = "dc.ai.ollama.catalog.model", defaultValue = "")
String ollamaCatalogModel;
@ConfigProperty(name = "dc.ai.ollama.ocr.model", defaultValue = "")
String ollamaOcrModel;
// --- Mistral ---
@ConfigProperty(name = "dc.ai.mistral.api-key", defaultValue = "")
String mistralApiKey;
@ConfigProperty(name = "dc.ai.mistral.main.model", defaultValue = "mistral-small-latest")
String mistralMainModel;
@ConfigProperty(name = "dc.ai.mistral.catalog.model", defaultValue = "mistral-small-latest")
String mistralCatalogModel;
@ConfigProperty(name = "dc.ai.mistral.ocr.model", defaultValue = "pixtral-12b-2409")
String mistralOcrModel;
// --- Gemeinsame Parameter ---
@ConfigProperty(name = "dc.ai.main.timeout", defaultValue = "300s")
String mainTimeout;
@@ -42,9 +56,6 @@ public class AiModelProducer {
@ConfigProperty(name = "dc.ai.main.temperature", defaultValue = "0.1")
double mainTemperature;
@ConfigProperty(name = "dc.ai.catalog.model")
String catalogModelName;
@ConfigProperty(name = "dc.ai.catalog.timeout", defaultValue = "1200s")
String catalogTimeout;
@@ -54,9 +65,6 @@ public class AiModelProducer {
@ConfigProperty(name = "dc.ai.catalog.temperature", defaultValue = "0.1")
double catalogTemperature;
@ConfigProperty(name = "dc.ai.ocr.model")
String ocrModelName;
@ConfigProperty(name = "dc.ai.ocr.timeout", defaultValue = "120s")
String ocrTimeout;
@@ -69,68 +77,89 @@ public class AiModelProducer {
@ConfigProperty(name = "dc.ai.log-responses", defaultValue = "false")
boolean logResponses;
/**
* Hauptmodell: Auswertung der Fragen + Übersetzung
* Modell: gpt-oss:20b
*/
@Produces
@ApplicationScoped
@Named("main")
public ChatLanguageModel mainModel() {
if (isMistral()) {
return MistralAiChatModel.builder()
.apiKey(mistralApiKey)
.modelName(mistralMainModel)
.temperature(mainTemperature)
.timeout(parseDuration(mainTimeout))
.maxRetries(mainMaxRetries)
.logRequests(logRequests)
.logResponses(logResponses)
.build();
}
return OllamaChatModel.builder()
.baseUrl(baseUrl)
.modelName(mainModelName)
.baseUrl(ollamaBaseUrl)
.modelName(ollamaMainModel)
.temperature(mainTemperature)
.timeout(parseDuration(mainTimeout))
.maxRetries(mainMaxRetries)
.customHeaders(Map.of("X-API-KEY", apiKey))
.customHeaders(Map.of("X-API-KEY", ollamaApiKey))
.logRequests(logRequests)
.logResponses(logResponses)
.build();
}
/**
* Katalog-Modell: Vollständige Katalog-Generierung aus Regelwerk-Dokument.
* Längerer Timeout (1200s) notwendig da komplette Dokumente analysiert werden.
*/
@Produces
@ApplicationScoped
@Named("catalog")
public ChatLanguageModel catalogModel() {
if (isMistral()) {
return MistralAiChatModel.builder()
.apiKey(mistralApiKey)
.modelName(mistralCatalogModel)
.temperature(catalogTemperature)
.timeout(parseDuration(catalogTimeout))
.maxRetries(catalogMaxRetries)
.logRequests(logRequests)
.logResponses(logResponses)
.build();
}
return OllamaChatModel.builder()
.baseUrl(baseUrl)
.modelName(catalogModelName)
.baseUrl(ollamaBaseUrl)
.modelName(ollamaCatalogModel)
.temperature(catalogTemperature)
.timeout(parseDuration(catalogTimeout))
.maxRetries(catalogMaxRetries)
.customHeaders(Map.of("X-API-KEY", apiKey))
.customHeaders(Map.of("X-API-KEY", ollamaApiKey))
.logRequests(logRequests)
.logResponses(logResponses)
.build();
}
/**
* OCR-Modell: Bildtext-Extraktion aus PDF-Seiten (Vision-Modus)
* Modell: quen3.5:9b
*/
@Produces
@ApplicationScoped
@Named("ocr")
public ChatLanguageModel ocrModel() {
if (isMistral()) {
return MistralAiChatModel.builder()
.apiKey(mistralApiKey)
.modelName(mistralOcrModel)
.timeout(parseDuration(ocrTimeout))
.maxRetries(ocrMaxRetries)
.logRequests(logRequests)
.logResponses(logResponses)
.build();
}
return OllamaChatModel.builder()
.baseUrl(baseUrl)
.modelName(ocrModelName)
.baseUrl(ollamaBaseUrl)
.modelName(ollamaOcrModel)
.timeout(parseDuration(ocrTimeout))
.maxRetries(ocrMaxRetries)
.customHeaders(Map.of("X-API-KEY", apiKey))
.customHeaders(Map.of("X-API-KEY", ollamaApiKey))
.logRequests(logRequests)
.logResponses(logResponses)
.build();
}
/**
* Parst Timeout-Strings wie "300s", "5m", "120s" in Duration.
*/
private boolean isMistral() {
return "mistral".equalsIgnoreCase(provider);
}
private Duration parseDuration(String value) {
if (value.endsWith("s")) {
return Duration.ofSeconds(Long.parseLong(value.replace("s", "")));

View File

@@ -179,4 +179,16 @@ public interface OrdsClient {
@DELETE
@Path("/projects/{projectId}/results")
Response deleteResults(@PathParam("projectId") long projectId);
// =========================================================================
// KI-Kostenverfolgung
// =========================================================================
/**
* POST /api/dc/ai-costs
* Speichert einen KI-Aufruf mit Token-Verbrauch und Kosten in dc_ai_cost_log.
*/
@POST
@Path("/ai-costs")
Response logAiCost(AiCostRequest body);
}

View File

@@ -0,0 +1,41 @@
package de.frigosped.dc.model;
public class AiCostRequest {
private String provider;
private String modelName;
private String operation;
private Long projectId;
private Long catalogId;
private int promptTokens;
private int completionTokens;
private int totalTokens;
private double costEur;
public String getProvider() { return provider; }
public void setProvider(String v) { this.provider = v; }
public String getModelName() { return modelName; }
public void setModelName(String v) { this.modelName = v; }
public String getOperation() { return operation; }
public void setOperation(String v) { this.operation = v; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long v) { this.projectId = v; }
public Long getCatalogId() { return catalogId; }
public void setCatalogId(Long v) { this.catalogId = v; }
public int getPromptTokens() { return promptTokens; }
public void setPromptTokens(int v) { this.promptTokens = v; }
public int getCompletionTokens() { return completionTokens; }
public void setCompletionTokens(int v) { this.completionTokens = v; }
public int getTotalTokens() { return totalTokens; }
public void setTotalTokens(int v) { this.totalTokens = v; }
public double getCostEur() { return costEur; }
public void setCostEur(double v) { this.costEur = v; }
}

View File

@@ -0,0 +1,112 @@
package de.frigosped.dc.service;
import de.frigosped.dc.client.OrdsClient;
import de.frigosped.dc.model.AiCostRequest;
import dev.langchain4j.model.output.TokenUsage;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
import java.util.concurrent.CompletableFuture;
/**
* Erfasst Token-Verbrauch und Kosten für jeden KI-Aufruf und persistiert
* sie asynchron via ORDS in dc_ai_cost_log.
*
* Aufruf-Muster (qualifier = "main" | "catalog" | "ocr"):
* costTracker.track("main", "TRANSLATE", projectId, null, response.tokenUsage());
*/
@ApplicationScoped
public class AiCostTrackerService {
private static final Logger LOG = Logger.getLogger(AiCostTrackerService.class);
@ConfigProperty(name = "dc.ai.provider", defaultValue = "ollama")
String provider;
// Modellnamen je Provider
@ConfigProperty(name = "dc.ai.ollama.main.model", defaultValue = "") String ollamaMainModel;
@ConfigProperty(name = "dc.ai.ollama.catalog.model", defaultValue = "") String ollamaCatalogModel;
@ConfigProperty(name = "dc.ai.ollama.ocr.model", defaultValue = "") String ollamaOcrModel;
@ConfigProperty(name = "dc.ai.mistral.main.model", defaultValue = "mistral-small-latest") String mistralMainModel;
@ConfigProperty(name = "dc.ai.mistral.catalog.model", defaultValue = "mistral-small-latest") String mistralCatalogModel;
@ConfigProperty(name = "dc.ai.mistral.ocr.model", defaultValue = "pixtral-12b-2409") String mistralOcrModel;
// Preise in EUR pro 1 Mio. Tokens (Mistral; Ollama = 0)
@ConfigProperty(name = "dc.ai.mistral.main.price-input-per-1m-eur", defaultValue = "0.092") double mistralMainInput;
@ConfigProperty(name = "dc.ai.mistral.main.price-output-per-1m-eur", defaultValue = "0.276") double mistralMainOutput;
@ConfigProperty(name = "dc.ai.mistral.catalog.price-input-per-1m-eur", defaultValue = "0.092") double mistralCatalogInput;
@ConfigProperty(name = "dc.ai.mistral.catalog.price-output-per-1m-eur",defaultValue = "0.276") double mistralCatalogOutput;
@ConfigProperty(name = "dc.ai.mistral.ocr.price-input-per-1m-eur", defaultValue = "0.138") double mistralOcrInput;
@ConfigProperty(name = "dc.ai.mistral.ocr.price-output-per-1m-eur", defaultValue = "0.138") double mistralOcrOutput;
@Inject
@RestClient
OrdsClient ordsClient;
/**
* @param qualifier "main" | "catalog" | "ocr"
* @param operation "TRANSLATE" | "EVALUATE" | "OCR" | "CATALOG"
* @param projectId Projekt-ID (null bei Katalog-Operationen)
* @param catalogId Katalog-ID (null bei Prüf-Operationen)
* @param tokenUsage aus response.tokenUsage() null wird als 0 behandelt
*/
public void track(String qualifier, String operation,
Long projectId, Long catalogId, TokenUsage tokenUsage) {
int promptTokens = safeCount(tokenUsage != null ? tokenUsage.inputTokenCount() : null);
int completionTokens = safeCount(tokenUsage != null ? tokenUsage.outputTokenCount() : null);
int totalTokens = safeCount(tokenUsage != null ? tokenUsage.totalTokenCount() : null);
double costEur = calcCost(qualifier, promptTokens, completionTokens);
AiCostRequest req = new AiCostRequest();
req.setProvider(provider);
req.setModelName(resolveModel(qualifier));
req.setOperation(operation);
req.setProjectId(projectId);
req.setCatalogId(catalogId);
req.setPromptTokens(promptTokens);
req.setCompletionTokens(completionTokens);
req.setTotalTokens(totalTokens);
req.setCostEur(costEur);
CompletableFuture.runAsync(() -> {
try {
ordsClient.logAiCost(req);
LOG.debugf("Kosten erfasst: %s/%s %.6f EUR (%d tokens)",
qualifier, operation, costEur, totalTokens);
} catch (Exception e) {
LOG.warnf("Kostenverfolgung fehlgeschlagen (%s/%s): %s",
qualifier, operation, e.getMessage());
}
});
}
private String resolveModel(String qualifier) {
boolean isMistral = "mistral".equalsIgnoreCase(provider);
return switch (qualifier) {
case "catalog" -> isMistral ? mistralCatalogModel : ollamaCatalogModel;
case "ocr" -> isMistral ? mistralOcrModel : ollamaOcrModel;
default -> isMistral ? mistralMainModel : ollamaMainModel;
};
}
private double calcCost(String qualifier, int prompt, int completion) {
if (!"mistral".equalsIgnoreCase(provider)) return 0.0;
double inPrice, outPrice;
switch (qualifier) {
case "ocr" -> { inPrice = mistralOcrInput; outPrice = mistralOcrOutput; }
case "catalog" -> { inPrice = mistralCatalogInput; outPrice = mistralCatalogOutput; }
default -> { inPrice = mistralMainInput; outPrice = mistralMainOutput; }
}
return (prompt / 1_000_000.0) * inPrice
+ (completion / 1_000_000.0) * outPrice;
}
private int safeCount(Integer value) {
return value != null ? value : 0;
}
}

View File

@@ -56,6 +56,9 @@ public class CatalogGenerationService {
@Named("catalog")
ChatLanguageModel catalogModel;
@Inject
AiCostTrackerService costTracker;
@Inject
ObjectMapper objectMapper;
@@ -106,7 +109,7 @@ public class CatalogGenerationService {
catalogId, chunkNr, totalChunks, chunks.get(i).length());
GeneratedCatalogStructure chunkResult =
generateStructureFromChunk(catalogName, chunks.get(i), chunkNr, totalChunks);
generateStructureFromChunk(catalogName, chunks.get(i), chunkNr, totalChunks, catalogId);
if (chunkResult == null || chunkResult.getCategories() == null) {
LOG.warnf("[Katalog %d] Chunk %d lieferte kein Ergebnis übersprungen",
@@ -316,7 +319,8 @@ public class CatalogGenerationService {
private GeneratedCatalogStructure generateStructureFromChunk(String catalogName,
String chunkText,
int chunkNr,
int totalChunks) {
int totalChunks,
long catalogId) {
List<ChatMessage> messages = List.of(
SystemMessage.from(buildChunkSystemPrompt(chunkNr, totalChunks)),
UserMessage.from(
@@ -327,6 +331,7 @@ public class CatalogGenerationService {
);
Response<AiMessage> response = catalogModel.generate(messages);
costTracker.track("catalog", "CATALOG", null, catalogId, response.tokenUsage());
String rawJson = response.content().text();
return parseCatalogStructure(rawJson, catalogName + " Chunk " + chunkNr);
}

View File

@@ -151,11 +151,11 @@ public class CheckOrchestrationService {
byte[] fileBytes = fileResp.readEntity(byte[].class);
originalText = documentProcessingService.extractText(
fileBytes, doc.getMimeType(), doc.getFilename());
fileBytes, doc.getMimeType(), doc.getFilename(), projectId);
LOG.debugf("Dokument %d: %d Zeichen extrahiert",
(long) doc.getId(), (long) originalText.length());
translatedText = evaluationService.translate(originalText);
translatedText = evaluationService.translate(originalText, projectId);
LOG.debugf("Dokument %d: Übersetzung fertig (%d Zeichen)",
(long) doc.getId(), (long) translatedText.length());
@@ -204,7 +204,7 @@ public class CheckOrchestrationService {
LOG.debugf("Auswertung: Dok %d × Frage %d",
doc.getId(), question.getQuestionId());
EvaluationResult result = evaluationService.evaluate(question, originalText);
EvaluationResult result = evaluationService.evaluate(question, originalText, projectId);
int deviation = 0;
int warning = 0;

View File

@@ -46,6 +46,9 @@ public class DocumentProcessingService {
@Named("ocr")
ChatLanguageModel ocrModel;
@Inject
AiCostTrackerService costTracker;
// =========================================================================
// Öffentliche API
// =========================================================================
@@ -59,13 +62,17 @@ public class DocumentProcessingService {
* @return Markdown-String
*/
public String extractText(byte[] fileBytes, String mimeType, String filename) {
return extractText(fileBytes, mimeType, filename, null);
}
public String extractText(byte[] fileBytes, String mimeType, String filename, Long projectId) {
String effectiveMime = resolveMimeType(mimeType, filename);
LOG.debugf("Extrahiere Text: %s (%s)", filename, effectiveMime);
try {
return switch (effectiveMime) {
case "application/pdf"
-> extractFromPdf(fileBytes);
-> extractFromPdf(fileBytes, projectId);
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword"
-> extractFromDocx(fileBytes);
@@ -88,7 +95,7 @@ public class DocumentProcessingService {
// PDF: Seiten rendern + KI-OCR
// =========================================================================
private String extractFromPdf(byte[] fileBytes) throws Exception {
private String extractFromPdf(byte[] fileBytes, Long projectId) throws Exception {
LOG.debug("Starte PDF-OCR...");
StringBuilder result = new StringBuilder();
@@ -101,12 +108,11 @@ public class DocumentProcessingService {
LOG.debugf("OCR Seite %d/%d", i + 1, pageCount);
BufferedImage image = renderer.renderImageWithDPI(i, OCR_DPI, ImageType.RGB);
// Bild in PNG-Base64 umwandeln
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", baos);
String base64Image = Base64.getEncoder().encodeToString(baos.toByteArray());
String pageText = ocrPage(base64Image, i + 1, pageCount);
String pageText = ocrPage(base64Image, i + 1, pageCount, projectId);
result.append(pageText).append("\n\n");
}
}
@@ -114,10 +120,7 @@ public class DocumentProcessingService {
return result.toString().trim();
}
/**
* Sendet eine PDF-Seite als Base64-Bild an das Vision-Modell.
*/
private String ocrPage(String base64Image, int pageNum, int totalPages) {
private String ocrPage(String base64Image, int pageNum, int totalPages, Long projectId) {
UserMessage message = UserMessage.from(
ImageContent.from(base64Image, "image/png"),
TextContent.from(
@@ -134,7 +137,10 @@ public class DocumentProcessingService {
)
);
return ocrModel.generate(List.of(message)).content().text();
dev.langchain4j.model.output.Response<dev.langchain4j.data.message.AiMessage> response =
ocrModel.generate(List.of(message));
costTracker.track("ocr", "OCR", projectId, null, response.tokenUsage());
return response.content().text();
}
// =========================================================================

View File

@@ -39,6 +39,9 @@ public class EvaluationService {
@Named("main")
ChatLanguageModel mainModel;
@Inject
AiCostTrackerService costTracker;
@Inject
ObjectMapper objectMapper;
@@ -51,9 +54,10 @@ public class EvaluationService {
* Wenn der Text bereits auf Deutsch ist, wird er unverändert zurückgegeben.
*
* @param originalText Markdown-Text in Originalsprache
* @param projectId Für Kostenzuordnung
* @return Markdown-Text auf Deutsch
*/
public String translate(String originalText) {
public String translate(String originalText, long projectId) {
if (originalText == null || originalText.isBlank()) return "";
LOG.debugf("Starte Übersetzung (%d Zeichen)...", originalText.length());
@@ -71,6 +75,7 @@ public class EvaluationService {
);
Response<AiMessage> response = mainModel.generate(messages);
costTracker.track("main", "TRANSLATE", projectId, null, response.tokenUsage());
return response.content().text().trim();
}
@@ -88,7 +93,7 @@ public class EvaluationService {
* @param documentText Originaltext des Dokuments (Markdown)
* @return Strukturiertes Auswertungsergebnis
*/
public EvaluationResult evaluate(OrdsQuestion question, String documentText) {
public EvaluationResult evaluate(OrdsQuestion question, String documentText, long projectId) {
LOG.debugf("Werte Frage %d aus: %s",
question.getQuestionId(),
abbreviate(question.getQuestionText(), 80));
@@ -102,6 +107,7 @@ public class EvaluationService {
);
Response<AiMessage> response = mainModel.generate(messages);
costTracker.track("main", "EVALUATE", projectId, null, response.tokenUsage());
String rawText = response.content().text();
return parseEvaluationResult(rawText, question);

View File

@@ -12,25 +12,55 @@ quarkus.http.port=8090
dc.api.key=${DC_API_KEY:}
# =============================================================================
# KI-Modelle (Ollama-kompatibler Endpunkt)
# KI-Provider: ollama | mistral
# =============================================================================
dc.ai.provider=mistral
# =============================================================================
# Ollama-Konfiguration (aktiv wenn dc.ai.provider=ollama)
# =============================================================================
dc.ai.ollama.base-url=https://ollama.aquantico.de
dc.ai.ollama.api-key=324GF44-50AA-4B57-9386-K435DLJ764DFR
dc.ai.ollama.main.model=qwen3.6:35b-a3b-q4_K_M
dc.ai.ollama.catalog.model=qwen3.6:35b-a3b-q4_K_M
dc.ai.ollama.ocr.model=qwen3.6:35b-a3b-q4_K_M
# =============================================================================
# Mistral-Konfiguration (aktiv wenn dc.ai.provider=mistral)
# =============================================================================
dc.ai.mistral.api-key=${MISTRAL_API_KEY:2AkoohJC8MjfjhfLGObQESZs9jxbDnc0}
dc.ai.mistral.main.model=mistral-small-latest
dc.ai.mistral.catalog.model=mistral-small-latest
# pixtral-12b-2409 unterstützt Vision (PDF-Seiten als Bild)
dc.ai.mistral.ocr.model=pixtral-12b-2409
# Preise in EUR pro 1 Mio. Tokens (Stand 2025, 1 USD = 0.92 EUR)
# mistral-small-latest: $0.10/$0.30 pro 1M tokens
dc.ai.mistral.main.price-input-per-1m-eur=0.092
dc.ai.mistral.main.price-output-per-1m-eur=0.276
dc.ai.mistral.catalog.price-input-per-1m-eur=0.092
dc.ai.mistral.catalog.price-output-per-1m-eur=0.276
# pixtral-12b-2409: $0.15/$0.15 pro 1M tokens
dc.ai.mistral.ocr.price-input-per-1m-eur=0.138
dc.ai.mistral.ocr.price-output-per-1m-eur=0.138
# =============================================================================
# Gemeinsame Modell-Parameter (für beide Provider)
# =============================================================================
dc.ai.base-url=https://ollama.aquantico.de
dc.ai.api-key=324GF44-50AA-4B57-9386-K435DLJ764DFR
# Hauptmodell: Auswertung + Übersetzung
dc.ai.main.model=qwen3.6:35b-a3b-q4_K_M
dc.ai.main.timeout=300s
dc.ai.main.max-retries=2
dc.ai.main.temperature=0.1
# Katalog-Generierungsmodell: längerer Timeout für vollständige Katalog-Analyse
dc.ai.catalog.model=qwen3.6:35b-a3b-q4_K_M
# Katalog-Generierungsmodell
dc.ai.catalog.timeout=1200s
dc.ai.catalog.max-retries=1
dc.ai.catalog.temperature=0.1
# OCR-Modell: Bildtext-Extraktion aus PDF-Seiten
dc.ai.ocr.model=qwen3.6:35b-a3b-q4_K_M
dc.ai.ocr.timeout=120s
dc.ai.ocr.max-retries=2

View File

@@ -12,25 +12,55 @@ quarkus.http.port=8090
dc.api.key=${DC_API_KEY:}
# =============================================================================
# KI-Modelle (Ollama-kompatibler Endpunkt)
# KI-Provider: ollama | mistral
# =============================================================================
dc.ai.provider=mistral
# =============================================================================
# Ollama-Konfiguration (aktiv wenn dc.ai.provider=ollama)
# =============================================================================
dc.ai.ollama.base-url=https://ollama.aquantico.de
dc.ai.ollama.api-key=324GF44-50AA-4B57-9386-K435DLJ764DFR
dc.ai.ollama.main.model=qwen3.6:35b-a3b-q4_K_M
dc.ai.ollama.catalog.model=qwen3.6:35b-a3b-q4_K_M
dc.ai.ollama.ocr.model=qwen3.6:35b-a3b-q4_K_M
# =============================================================================
# Mistral-Konfiguration (aktiv wenn dc.ai.provider=mistral)
# =============================================================================
dc.ai.mistral.api-key=${MISTRAL_API_KEY:2AkoohJC8MjfjhfLGObQESZs9jxbDnc0}
dc.ai.mistral.main.model=mistral-small-latest
dc.ai.mistral.catalog.model=mistral-small-latest
# pixtral-12b-2409 unterstützt Vision (PDF-Seiten als Bild)
dc.ai.mistral.ocr.model=pixtral-12b-2409
# Preise in EUR pro 1 Mio. Tokens (Stand 2025, 1 USD = 0.92 EUR)
# mistral-small-latest: $0.10/$0.30 pro 1M tokens
dc.ai.mistral.main.price-input-per-1m-eur=0.092
dc.ai.mistral.main.price-output-per-1m-eur=0.276
dc.ai.mistral.catalog.price-input-per-1m-eur=0.092
dc.ai.mistral.catalog.price-output-per-1m-eur=0.276
# pixtral-12b-2409: $0.15/$0.15 pro 1M tokens
dc.ai.mistral.ocr.price-input-per-1m-eur=0.138
dc.ai.mistral.ocr.price-output-per-1m-eur=0.138
# =============================================================================
# Gemeinsame Modell-Parameter (für beide Provider)
# =============================================================================
dc.ai.base-url=https://ollama.aquantico.de
dc.ai.api-key=324GF44-50AA-4B57-9386-K435DLJ764DFR
# Hauptmodell: Auswertung + Übersetzung
dc.ai.main.model=qwen3.6:35b-a3b-q4_K_M
dc.ai.main.timeout=300s
dc.ai.main.max-retries=2
dc.ai.main.temperature=0.1
# Katalog-Generierungsmodell: längerer Timeout für vollständige Katalog-Analyse
dc.ai.catalog.model=qwen3.6:35b-a3b-q4_K_M
# Katalog-Generierungsmodell
dc.ai.catalog.timeout=1200s
dc.ai.catalog.max-retries=1
dc.ai.catalog.temperature=0.1
# OCR-Modell: Bildtext-Extraktion aus PDF-Seiten
dc.ai.ocr.model=qwen3.6:35b-a3b-q4_K_M
dc.ai.ocr.timeout=120s
dc.ai.ocr.max-retries=2

View File

@@ -9,6 +9,7 @@ de/frigosped/dc/service/MarkdownExportService.class
de/frigosped/dc/service/DocumentProcessingService.class
de/frigosped/dc/model/CatalogGenerateResponse.class
de/frigosped/dc/service/MarkdownExportService$Seg.class
de/frigosped/dc/model/AiCostRequest.class
de/frigosped/dc/model/OrdsCatalogStatusResponse.class
de/frigosped/dc/model/OrdsDocumentTexts.class
de/frigosped/dc/resource/CatalogResource.class
@@ -21,3 +22,4 @@ de/frigosped/dc/client/OrdsLoggingFilter.class
de/frigosped/dc/service/MarkdownExportService$PdfWriter.class
de/frigosped/dc/security/ApiKeyFilter.class
de/frigosped/dc/model/GeneratedCatalogStructure$GeneratedQuestion.class
de/frigosped/dc/service/AiCostTrackerService.class

View File

@@ -1,6 +1,7 @@
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/client/OrdsLoggingFilter.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/AiCostRequest.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/CatalogGenerateResponse.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/EvaluationResult.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/model/ExportRequest.java
@@ -23,6 +24,7 @@
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/resource/CheckResource.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/resource/ExportResource.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/security/ApiKeyFilter.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/AiCostTrackerService.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/CatalogGenerationService.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/CheckOrchestrationService.java
/mnt/c/source/Frigosped/Dokumenten-Check/dc-backend/src/main/java/de/frigosped/dc/service/DocumentProcessingService.java