diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 36fa764..adbc757 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -33,7 +33,8 @@
"Bash(NLS_LANG=GERMAN_GERMANY.AL32UTF8 sql *)",
"Bash(KUBECONFIG=/mnt/c/source/Frigosped/Dokumenten-Check/env/frigo-dev.yaml kubectl logs *)",
"Bash(KUBECONFIG=/mnt/c/source/Frigosped/Dokumenten-Check/env/frigo-dev.yaml kubectl get pods -n ai-env -l app=dc-backend)",
- "Bash(chmod +x *)"
+ "Bash(chmod +x *)",
+ "Bash(JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 mvn compile -q)"
]
}
}
diff --git a/Scripts/KI-Kostenverfolgung.sql b/Scripts/KI-Kostenverfolgung.sql
new file mode 100644
index 0000000..cbca21b
--- /dev/null
+++ b/Scripts/KI-Kostenverfolgung.sql
@@ -0,0 +1,143 @@
+-- =============================================================================
+-- KI-Kostenverfolgung für Dokumenten-Check
+-- Tabelle: dc_ai_cost_log
+-- Package: DC_COSTS_PKG
+-- =============================================================================
+-- Deployment:
+-- sqlplus user/pass@db @"Scripts/KI-Kostenverfolgung.sql"
+--
+-- Skript ist idempotent (DROP IF EXISTS vor jedem CREATE).
+-- =============================================================================
+
+
+-- =============================================================================
+-- Tabelle: dc_ai_cost_log
+-- =============================================================================
+BEGIN
+ EXECUTE IMMEDIATE 'DROP TABLE dc_ai_cost_log CASCADE CONSTRAINTS';
+EXCEPTION WHEN OTHERS THEN NULL;
+END;
+/
+
+CREATE TABLE dc_ai_cost_log (
+ id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ called_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
+ provider VARCHAR2(30) NOT NULL, -- mistral | ollama
+ model_name VARCHAR2(100) NOT NULL,
+ operation VARCHAR2(30) NOT NULL, -- TRANSLATE | EVALUATE | OCR | CATALOG
+ project_id NUMBER,
+ catalog_id NUMBER,
+ prompt_tokens NUMBER DEFAULT 0 NOT NULL,
+ completion_tokens NUMBER DEFAULT 0 NOT NULL,
+ total_tokens NUMBER DEFAULT 0 NOT NULL,
+ cost_eur NUMBER(14, 8) DEFAULT 0 NOT NULL,
+ created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
+ created_by VARCHAR2(200) DEFAULT 'SYSTEM' NOT NULL
+);
+
+COMMENT ON TABLE dc_ai_cost_log IS 'Protokoll aller KI-API-Aufrufe mit Token-Verbrauch und Kosten';
+COMMENT ON COLUMN dc_ai_cost_log.operation IS 'TRANSLATE, EVALUATE, OCR, CATALOG';
+COMMENT ON COLUMN dc_ai_cost_log.cost_eur IS 'Berechnete Kosten in EUR basierend auf konfigurierten Preisen';
+
+CREATE INDEX idx_ai_cost_called_at ON dc_ai_cost_log (called_at);
+CREATE INDEX idx_ai_cost_project_id ON dc_ai_cost_log (project_id);
+CREATE INDEX idx_ai_cost_catalog_id ON dc_ai_cost_log (catalog_id);
+CREATE INDEX idx_ai_cost_ym ON dc_ai_cost_log (TO_CHAR(called_at, 'YYYY-MM'));
+
+
+-- =============================================================================
+-- PL/SQL Package: DC_COSTS_PKG
+-- =============================================================================
+CREATE OR REPLACE PACKAGE DC_COSTS_PKG AS
+
+ -- -------------------------------------------------------------------------
+ -- Gesamtkosten + Kosten des laufenden Monats
+ -- -------------------------------------------------------------------------
+ PROCEDURE get_summary(
+ p_total_eur OUT NUMBER,
+ p_total_tokens OUT NUMBER,
+ p_total_calls OUT NUMBER,
+ p_month_eur OUT NUMBER,
+ p_month_tokens OUT NUMBER,
+ p_month_calls OUT NUMBER
+ );
+
+ -- -------------------------------------------------------------------------
+ -- Kosten pro Monat + Modell (für APEX-Reports / direkte Abfragen)
+ -- Gibt einen REF CURSOR zurück (Spalten: month, provider, model_name,
+ -- operation, call_count, prompt_tokens, completion_tokens, total_tokens, cost_eur)
+ -- -------------------------------------------------------------------------
+ FUNCTION get_monthly_detail RETURN SYS_REFCURSOR;
+
+END DC_COSTS_PKG;
+/
+
+CREATE OR REPLACE PACKAGE BODY DC_COSTS_PKG AS
+
+ PROCEDURE get_summary(
+ p_total_eur OUT NUMBER,
+ p_total_tokens OUT NUMBER,
+ p_total_calls OUT NUMBER,
+ p_month_eur OUT NUMBER,
+ p_month_tokens OUT NUMBER,
+ p_month_calls OUT NUMBER
+ ) IS
+ BEGIN
+ -- Gesamtkosten (all time)
+ SELECT NVL(SUM(cost_eur), 0),
+ NVL(SUM(total_tokens), 0),
+ COUNT(*)
+ INTO p_total_eur, p_total_tokens, p_total_calls
+ FROM dc_ai_cost_log;
+
+ -- Laufender Monat
+ SELECT NVL(SUM(cost_eur), 0),
+ NVL(SUM(total_tokens), 0),
+ COUNT(*)
+ INTO p_month_eur, p_month_tokens, p_month_calls
+ FROM dc_ai_cost_log
+ WHERE called_at >= TRUNC(SYSDATE, 'MM');
+
+ EXCEPTION
+ WHEN NO_DATA_FOUND THEN
+ p_total_eur := 0; p_total_tokens := 0; p_total_calls := 0;
+ p_month_eur := 0; p_month_tokens := 0; p_month_calls := 0;
+ END get_summary;
+
+
+ FUNCTION get_monthly_detail RETURN SYS_REFCURSOR IS
+ v_cur SYS_REFCURSOR;
+ BEGIN
+ OPEN v_cur FOR
+ SELECT
+ TO_CHAR(called_at, 'YYYY-MM') AS month,
+ provider,
+ model_name,
+ operation,
+ COUNT(*) AS call_count,
+ SUM(prompt_tokens) AS prompt_tokens,
+ SUM(completion_tokens) AS completion_tokens,
+ SUM(total_tokens) AS total_tokens,
+ ROUND(SUM(cost_eur), 6) AS cost_eur
+ FROM dc_ai_cost_log
+ WHERE called_at >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -11)
+ GROUP BY TO_CHAR(called_at, 'YYYY-MM'), provider, model_name, operation
+ ORDER BY month DESC, cost_eur DESC;
+ RETURN v_cur;
+ END get_monthly_detail;
+
+END DC_COSTS_PKG;
+/
+
+-- =============================================================================
+-- Kurztest (optionale Verifikation nach Deployment)
+-- =============================================================================
+-- DECLARE
+-- v_te NUMBER; v_tt NUMBER; v_tc NUMBER;
+-- v_me NUMBER; v_mt NUMBER; v_mc NUMBER;
+-- BEGIN
+-- DC_COSTS_PKG.get_summary(v_te,v_tt,v_tc,v_me,v_mt,v_mc);
+-- DBMS_OUTPUT.PUT_LINE('Total EUR: ' || v_te || ', Calls: ' || v_tc);
+-- DBMS_OUTPUT.PUT_LINE('Month EUR: ' || v_me || ', Calls: ' || v_mc);
+-- END;
+-- /
diff --git a/Scripts/ORDS REST Services.sql b/Scripts/ORDS REST Services.sql
index 8636e75..c1016eb 100644
--- a/Scripts/ORDS REST Services.sql
+++ b/Scripts/ORDS REST Services.sql
@@ -1026,6 +1026,136 @@ BEGIN
]'
);
+ -- ===========================================================================
+ -- TEMPLATE 17: ai-costs (POST = Eintrag speichern, GET = Auswertung)
+ -- ===========================================================================
+ ORDS.DEFINE_TEMPLATE(
+ p_module_name => 'frigosped.dc',
+ p_pattern => 'ai-costs',
+ p_priority => 0,
+ p_comments => 'KI-Kostenprotokoll: POST speichert Aufruf, GET liefert Auswertung'
+ );
+
+ -- 17a. POST /api/dc/ai-costs
+ -- Body: {"provider":"mistral","model_name":"mistral-small-latest","operation":"TRANSLATE",
+ -- "project_id":42,"catalog_id":null,
+ -- "prompt_tokens":1234,"completion_tokens":567,"total_tokens":1801,"cost_eur":0.000157}
+ ORDS.DEFINE_HANDLER(
+ p_module_name => 'frigosped.dc',
+ p_pattern => 'ai-costs',
+ p_method => 'POST',
+ p_source_type => ORDS.SOURCE_TYPE_PLSQL,
+ p_comments => '201 Created mit neuer cost_log id',
+ p_source => q'[
+ DECLARE
+ v_body CLOB;
+ v_new_id NUMBER;
+ BEGIN
+ v_body := :body_text;
+
+ INSERT INTO dc_ai_cost_log (
+ provider, model_name, operation,
+ project_id, catalog_id,
+ prompt_tokens, completion_tokens, total_tokens,
+ cost_eur
+ ) VALUES (
+ JSON_VALUE(v_body, '$.provider'),
+ JSON_VALUE(v_body, '$.model_name'),
+ JSON_VALUE(v_body, '$.operation'),
+ TO_NUMBER(JSON_VALUE(v_body, '$.project_id')),
+ TO_NUMBER(JSON_VALUE(v_body, '$.catalog_id')),
+ NVL(TO_NUMBER(JSON_VALUE(v_body, '$.prompt_tokens')), 0),
+ NVL(TO_NUMBER(JSON_VALUE(v_body, '$.completion_tokens')), 0),
+ NVL(TO_NUMBER(JSON_VALUE(v_body, '$.total_tokens')), 0),
+ NVL(TO_NUMBER(JSON_VALUE(v_body, '$.cost_eur')), 0)
+ )
+ RETURNING id INTO v_new_id;
+
+ :status_code := 201;
+ HTP.P('{"id":' || v_new_id || '}');
+
+ EXCEPTION
+ WHEN OTHERS THEN
+ ROLLBACK;
+ :status_code := 500;
+ HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
+ END;
+ ]'
+ );
+
+ -- 17b. GET /api/dc/ai-costs
+ -- Liefert: Gesamtkosten, laufender Monat, Monats-Aufschlüsselung (12 Monate)
+ ORDS.DEFINE_HANDLER(
+ p_module_name => 'frigosped.dc',
+ p_pattern => 'ai-costs',
+ p_method => 'GET',
+ p_source_type => ORDS.SOURCE_TYPE_PLSQL,
+ p_comments => 'Kostenauswertung: total, current_month, monatliche Aufschluesselung via DC_COSTS_PKG',
+ p_source => q'[
+ DECLARE
+ v_total_eur NUMBER;
+ v_total_tokens NUMBER;
+ v_total_calls NUMBER;
+ v_month_eur NUMBER;
+ v_month_tokens NUMBER;
+ v_month_calls NUMBER;
+ v_monthly_json CLOB;
+ BEGIN
+ DC_COSTS_PKG.get_summary(
+ v_total_eur, v_total_tokens, v_total_calls,
+ v_month_eur, v_month_tokens, v_month_calls
+ );
+
+ SELECT JSON_ARRAYAGG(
+ JSON_OBJECT(
+ 'month' VALUE month,
+ 'cost_eur' VALUE ROUND(cost_eur, 6),
+ 'prompt_tokens' VALUE prompt_tokens,
+ 'completion_tokens' VALUE completion_tokens,
+ 'total_tokens' VALUE total_tokens,
+ 'call_count' VALUE call_count
+ )
+ ORDER BY month DESC
+ RETURNING CLOB
+ )
+ INTO v_monthly_json
+ FROM (
+ SELECT
+ TO_CHAR(called_at, 'YYYY-MM') AS month,
+ SUM(cost_eur) AS cost_eur,
+ SUM(prompt_tokens) AS prompt_tokens,
+ SUM(completion_tokens) AS completion_tokens,
+ SUM(total_tokens) AS total_tokens,
+ COUNT(*) AS call_count
+ FROM dc_ai_cost_log
+ WHERE called_at >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -11)
+ GROUP BY TO_CHAR(called_at, 'YYYY-MM')
+ );
+
+ :status_code := 200;
+ HTP.P(
+ '{"total":{'
+ || '"cost_eur":' || ROUND(NVL(v_total_eur, 0), 6)
+ || ',"total_tokens":'|| NVL(v_total_tokens, 0)
+ || ',"call_count":' || NVL(v_total_calls, 0)
+ || '},"current_month":{'
+ || '"month":"' || TO_CHAR(SYSDATE, 'YYYY-MM') || '"'
+ || ',"cost_eur":' || ROUND(NVL(v_month_eur, 0), 6)
+ || ',"total_tokens":'|| NVL(v_month_tokens, 0)
+ || ',"call_count":' || NVL(v_month_calls, 0)
+ || '},"monthly":' || NVL(v_monthly_json, '[]')
+ || '}'
+ );
+
+ EXCEPTION
+ WHEN OTHERS THEN
+ :status_code := 500;
+ HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
+ END;
+ ]'
+ );
+
+
COMMIT;
END;
/
diff --git a/dc-backend/pom.xml b/dc-backend/pom.xml
index 5583b63..92c5416 100644
--- a/dc-backend/pom.xml
+++ b/dc-backend/pom.xml
@@ -73,6 +73,11 @@
langchain4j-ollama
${langchain4j.version}
+
+ dev.langchain4j
+ langchain4j-mistral-ai
+ ${langchain4j.version}
+
diff --git a/dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java b/dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
index 7ce042e..630b392 100644
--- a/dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
+++ b/dc-backend/src/main/java/de/frigosped/dc/ai/AiModelProducer.java
@@ -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", "")));
diff --git a/dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java b/dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
index 16c35af..f7b5550 100644
--- a/dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
+++ b/dc-backend/src/main/java/de/frigosped/dc/client/OrdsClient.java
@@ -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);
}
diff --git a/dc-backend/src/main/java/de/frigosped/dc/model/AiCostRequest.java b/dc-backend/src/main/java/de/frigosped/dc/model/AiCostRequest.java
new file mode 100644
index 0000000..69858d9
--- /dev/null
+++ b/dc-backend/src/main/java/de/frigosped/dc/model/AiCostRequest.java
@@ -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; }
+}
diff --git a/dc-backend/src/main/java/de/frigosped/dc/service/AiCostTrackerService.java b/dc-backend/src/main/java/de/frigosped/dc/service/AiCostTrackerService.java
new file mode 100644
index 0000000..16ebd68
--- /dev/null
+++ b/dc-backend/src/main/java/de/frigosped/dc/service/AiCostTrackerService.java
@@ -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;
+ }
+}
diff --git a/dc-backend/src/main/java/de/frigosped/dc/service/CatalogGenerationService.java b/dc-backend/src/main/java/de/frigosped/dc/service/CatalogGenerationService.java
index be02237..c1eefcd 100644
--- a/dc-backend/src/main/java/de/frigosped/dc/service/CatalogGenerationService.java
+++ b/dc-backend/src/main/java/de/frigosped/dc/service/CatalogGenerationService.java
@@ -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 messages = List.of(
SystemMessage.from(buildChunkSystemPrompt(chunkNr, totalChunks)),
UserMessage.from(
@@ -327,6 +331,7 @@ public class CatalogGenerationService {
);
Response response = catalogModel.generate(messages);
+ costTracker.track("catalog", "CATALOG", null, catalogId, response.tokenUsage());
String rawJson = response.content().text();
return parseCatalogStructure(rawJson, catalogName + " Chunk " + chunkNr);
}
diff --git a/dc-backend/src/main/java/de/frigosped/dc/service/CheckOrchestrationService.java b/dc-backend/src/main/java/de/frigosped/dc/service/CheckOrchestrationService.java
index e6e5e8e..4645502 100644
--- a/dc-backend/src/main/java/de/frigosped/dc/service/CheckOrchestrationService.java
+++ b/dc-backend/src/main/java/de/frigosped/dc/service/CheckOrchestrationService.java
@@ -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;
diff --git a/dc-backend/src/main/java/de/frigosped/dc/service/DocumentProcessingService.java b/dc-backend/src/main/java/de/frigosped/dc/service/DocumentProcessingService.java
index 2c198ef..a743a73 100644
--- a/dc-backend/src/main/java/de/frigosped/dc/service/DocumentProcessingService.java
+++ b/dc-backend/src/main/java/de/frigosped/dc/service/DocumentProcessingService.java
@@ -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 response =
+ ocrModel.generate(List.of(message));
+ costTracker.track("ocr", "OCR", projectId, null, response.tokenUsage());
+ return response.content().text();
}
// =========================================================================
diff --git a/dc-backend/src/main/java/de/frigosped/dc/service/EvaluationService.java b/dc-backend/src/main/java/de/frigosped/dc/service/EvaluationService.java
index b442857..dc099e5 100644
--- a/dc-backend/src/main/java/de/frigosped/dc/service/EvaluationService.java
+++ b/dc-backend/src/main/java/de/frigosped/dc/service/EvaluationService.java
@@ -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 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 response = mainModel.generate(messages);
+ costTracker.track("main", "EVALUATE", projectId, null, response.tokenUsage());
String rawText = response.content().text();
return parseEvaluationResult(rawText, question);
diff --git a/dc-backend/src/main/resources/application.properties b/dc-backend/src/main/resources/application.properties
index b9d65bc..2baab1e 100644
--- a/dc-backend/src/main/resources/application.properties
+++ b/dc-backend/src/main/resources/application.properties
@@ -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
diff --git a/dc-backend/target/classes/application.properties b/dc-backend/target/classes/application.properties
index b9d65bc..2baab1e 100644
--- a/dc-backend/target/classes/application.properties
+++ b/dc-backend/target/classes/application.properties
@@ -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
diff --git a/dc-backend/target/classes/de/frigosped/dc/ai/AiModelProducer.class b/dc-backend/target/classes/de/frigosped/dc/ai/AiModelProducer.class
index 00a5781..547f242 100644
Binary files a/dc-backend/target/classes/de/frigosped/dc/ai/AiModelProducer.class and b/dc-backend/target/classes/de/frigosped/dc/ai/AiModelProducer.class differ
diff --git a/dc-backend/target/classes/de/frigosped/dc/client/OrdsClient.class b/dc-backend/target/classes/de/frigosped/dc/client/OrdsClient.class
index 57bbf49..2ce67b0 100644
Binary files a/dc-backend/target/classes/de/frigosped/dc/client/OrdsClient.class and b/dc-backend/target/classes/de/frigosped/dc/client/OrdsClient.class differ
diff --git a/dc-backend/target/classes/de/frigosped/dc/service/CheckOrchestrationService.class b/dc-backend/target/classes/de/frigosped/dc/service/CheckOrchestrationService.class
index 4d0beac..4b58c06 100644
Binary files a/dc-backend/target/classes/de/frigosped/dc/service/CheckOrchestrationService.class and b/dc-backend/target/classes/de/frigosped/dc/service/CheckOrchestrationService.class differ
diff --git a/dc-backend/target/classes/de/frigosped/dc/service/DocumentProcessingService.class b/dc-backend/target/classes/de/frigosped/dc/service/DocumentProcessingService.class
index eac5b01..8a07175 100644
Binary files a/dc-backend/target/classes/de/frigosped/dc/service/DocumentProcessingService.class and b/dc-backend/target/classes/de/frigosped/dc/service/DocumentProcessingService.class differ
diff --git a/dc-backend/target/classes/de/frigosped/dc/service/EvaluationService.class b/dc-backend/target/classes/de/frigosped/dc/service/EvaluationService.class
index 63696f7..95e0077 100644
Binary files a/dc-backend/target/classes/de/frigosped/dc/service/EvaluationService.class and b/dc-backend/target/classes/de/frigosped/dc/service/EvaluationService.class differ
diff --git a/dc-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/dc-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
index b39f459..545e599 100644
--- a/dc-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
+++ b/dc-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -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
diff --git a/dc-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/dc-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
index c3dd4dd..f3b4cd9 100644
--- a/dc-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
+++ b/dc-backend/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -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