From f3ae6859bf2b739e9e235ee23003d98c46c58fd8 Mon Sep 17 00:00:00 2001 From: Wolf Beckmann Date: Mon, 1 Jun 2026 14:21:42 +0200 Subject: [PATCH] Kosten Endpunkt --- .claude/settings.local.json | 3 +- Scripts/KI-Kostenverfolgung.sql | 143 ++++++++++++++++++ Scripts/ORDS REST Services.sql | 130 ++++++++++++++++ dc-backend/pom.xml | 5 + .../de/frigosped/dc/ai/AiModelProducer.java | 123 +++++++++------ .../de/frigosped/dc/client/OrdsClient.java | 12 ++ .../de/frigosped/dc/model/AiCostRequest.java | 41 +++++ .../dc/service/AiCostTrackerService.java | 112 ++++++++++++++ .../dc/service/CatalogGenerationService.java | 9 +- .../dc/service/CheckOrchestrationService.java | 6 +- .../dc/service/DocumentProcessingService.java | 24 +-- .../dc/service/EvaluationService.java | 10 +- .../src/main/resources/application.properties | 44 +++++- .../target/classes/application.properties | 44 +++++- .../de/frigosped/dc/ai/AiModelProducer.class | Bin 4406 -> 6401 bytes .../de/frigosped/dc/client/OrdsClient.class | Bin 3204 -> 3339 bytes .../service/CheckOrchestrationService.class | Bin 13195 -> 13219 bytes .../service/DocumentProcessingService.class | Bin 12842 -> 13770 bytes .../dc/service/EvaluationService.class | Bin 8999 -> 9572 bytes .../compile/default-compile/createdFiles.lst | 2 + .../compile/default-compile/inputFiles.lst | 2 + 21 files changed, 632 insertions(+), 78 deletions(-) create mode 100644 Scripts/KI-Kostenverfolgung.sql create mode 100644 dc-backend/src/main/java/de/frigosped/dc/model/AiCostRequest.java create mode 100644 dc-backend/src/main/java/de/frigosped/dc/service/AiCostTrackerService.java 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 00a57816d56688b4b75489877359cf448815b43c..547f24241dadac618fc05fc4438a9b9051825ca4 100644 GIT binary patch literal 6401 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 aY{Ng|3McP}WYVkr%X7Vl|MmDMy8i=1eVWYx 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 57bbf49c6fc4c9e4b49e4d44689e110725216b3d..2ce67b09828f904277b8272675a7cc372acfa2d2 100644 GIT binary patch delta 154 zcmZpX?3Us>^>5cc1_lO42H%ZbUhI6FIr-_1na=seC5#L%8k6Oj#n~X-$@f?^xH$C_ zGj)@JlEs^Y*!3701vfWx>|5cc1_lO42Ct1=UhJDUv+FQ23T%GBv5(o6fr-HZD8&Sn}2yMxhZ1 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 4d0beacc878df879f69bc2cd310d90464e7b1788..4b58c065ee79cbb5410b328734c04c9f205d0277 100644 GIT binary patch delta 934 zcmX|?n3_82 z2Ej;Ds17J%xX|tfkvh6MH?TIF_=TcND^c5rI(2g&+H}{iZM|;L`RDf@IOlnu_dVz9 z75i6IE%b3wbWx!r?-)1D@A)p^b$KC4eC`u+0e^^!61y^w8S_!p9S>!@wXbBj&B35+ zp|Hyt8%P`u9totbK^nqjCA{t^m1t+-}r}+b%;g6NeagJBxJa15L z!UZlvBX7eWyd9T#FD~;T{K>!KFK)o!+=wfD8Q1wL{^4dc@hvoSJ6gCKt=x+n{1`X6 zAGbtsN3w8N#-d#!=#V#*vv6N$^iG$&g>IRH9+{_{k6u}Xhq4lnWSw#Y`eYN5Qi`Wi zhG*gkI>3I}g#jtYpzKq|-H%fi$JSB@o+D`Xqnz$hCjvSxKi#Ev1k5)$MP1YlzZpX% z)T33VVT#cWx(S~)n4tUg03mI&ivFcGjMQciP_I^_H2MsBNRN=M4MeCh=@w$}Ia}C4zP~N0| zl@@H4muP?nk%F(pYoK>M`YvY;8G{kDjHer`u~MnQUNme3X>`G!Fe$XO6JBbw2aQJ$ zrt5rsLkaDR|4&!x8gxm88DUaECQUapAe4`B)MUI!9}cQjVA$5A|8JY2wyNuvOg%xR P?$x}>v?DP$uYmspDGm*> delta 902 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 delta 6364 zcma)A34B!5_5RMA<-M7C*(QWAA%Q?hfGmW~Aj2YIRhAGzHbrnq28d)c89c};bhhEmVQk0i3NDDPL*VomAB6UrT<;}HAy~sgd69wpJ4c7{!$eOQB4-63a zyXrNxu5N0nHBfBr(@IB|3cQi<#gX#n`cPe?fkA>4TW$!otf*~T-ROmZ!6t@as34V! zEuosoe{8&g^s*&!%du~#?rcsO>C;USj`(@;`D!?H?ub4K@g*Kj5aX_W33&I zX+{plnQ)=fI_Bsn6RZN~aHAV0nV5_z)?(-IoU;U(7lu}a%Iiao%gQHRToZ0qXErd+ zdeB)w-5DmTFw=U&Id#CKekyDPEk3*(T1x9BZm;fP0>a`B-3G?3(1O z#(8QF3x%GkG&47D^;sXgQu{9wWXz6)S|Z{6>WP!fW=@zjXwV=py5Ry7AuJKNYQsxb zF0;mH+4@`^wSsJ`*_-Y6q6Q8xmSMS$IujRSg|)zmD$aM)GHe>$t7L+Va9$ zwWCXwMpSKp`?p@veW_RAN*(J>{1iVE6k2=SxmMKhSSiL(qaIhAxCTGBrWiHy3u}kb zH?RQ?1J?^2)m2lx*oYf-SSD`5&DO_8qwiMSrel+d&Ddfs_EZ*cz%NbQfjb4hNYnT= zk#J>8OK6S2Ibh&<-e;l>zY^$8H7(VlWnsYpdz;_-vW*&S zH?ac`STnt&Mm;3Zyk6ywb`y`lO%K=9H?@X!>^cPn=PYk&S{+(aANFD=er+O#4(nNO zS#b?IP3*xOfe~q%9cig+T&D8kF%$b#r;|MmI-XE2UhK;&n9$T12{%RzJSp(hHE?I; z&1%Nect*#wCJx{^YoTw0`vrxRgVvY6o~bY4H#%NcL$9b)(26z8tn&O8zti!1c3T-{ zU+V!g?0(I}>v+S;P8sWa3vcUq$HbrTXX}ELGkovieH|Z|_zOO?wx_I0@#16rRmUeL zKE-F&)v3kSLuvUMzOY_RE6&8{_{zlB@$ECu418;)r1x{-Zwjm%Gx|v_oL`>1dVI58{^0~yVi*AY`9;+~;zuzT3Q%rnDuQqa9x};i*GRh_0x-Mg=+iyyy zbh8d*EcA63h*uZVS%0-#Yo_1h&M~E@VRxO&MTEncA}>YwE*>6f@>$hC{Us`4Ba^)RZzAWTj`8MF$h6 zOTsO!HOpJ-mPUs7p<$jO!%j{ZYRGA)=nVD4p>$3^sWZ%w;3+!8{BSCrF(-9~8*;`e zI>Y^Rt#6*%@_17w$VBV8%+g_$vt^1YXX1GLz_XfGxBBpwOf_X%;=q#{nj>pe-wac# zWTsWrE#FCYvm;I)y2?4nklDgrWwj|6%NpzK?#;eSWSuTQG38R;S}%1k zG%k_LOu1aHu)go!$G2X7s>{zzxr%IDm|bM7mupP{% zIB%=s^VWHFk>!T`k}fF7H{{M!ClC&IkqHab(A}pF5e4_sM+IukD|Ki$c}PSbxYb zl%Bhep4=GF$VgwO_>nqNyz z*GjB?eJhH~RM#L=%g|D(Fjz@LOfAKhhIzH&+G)C0L88`1Sa}7{rjGP#L2Z<-jW)G0 z+Spy+6y%AFw^9p>ofCz4%qZ+-om=RSI(R*>1Y=c}BM0O;h&<1>hRgVT9dA&6fvq%X zauARgDZkVzzd;Ik2)HS}%;9oMWG1+ZJ0Op+p~)+Ja+uE|d6iEyt{}ga-%%3zz5Ib% zFPBhPCy(gt8(Cah7K0vi?nH_|qb%TzAv53%y0i*+z!gKly1ghEb@_Y8&}S3oIx5_4 zs1CUNg|`dpq|wjUV;r z;-`?Gvc%^!evgT&M=?#5x1mCP4{O69^nX=sce!*Jy8Jml$|r=6aXQYF zcR4l1A+OW!oBX^@QB$Qfk44PLUm0~D`<_&OIzNWRdr`BX1K}AZyRo!tFX|VRb)Yfm zq@OOTP)_r&%5ue`xR`;@a%Z`=VX^AGB+Ko*fCKnRz?tRtU$zsM$8c3p--`{jc5ToI z=>F>h#%|m&Z#QlU7%|))!`7guIN&)k^WLDhG~kWle*c4f%v-7k@;e^8sqpiN3mLofnPZEo zbpd*_bS}gaCQU6yBFsQ8#T?XO5$ojTSiusw0av0C*K_=4G}G#0bfU!$XLFQ3n}eXd zC+{<77Ga2dAb+9xl}z6c88Hv1n(`4_UV6GlKBk}DTym9sBA?RZg{-?jW1ov#xko;i zFBomc-~=kzilZVOkI0wu6;o6pfG9dnbGnXbJlHilT6EM4;qy-{nW|&wab|cD3-RHd z3JW`p?`h;~8gUYAaa6cOp@L3hQLIRS?M>Vf@reyiyy@w1J2!evlH)Xtb0G&kI!THvR&D;)1|v@XAM2E7Ts|P5xIuvzs7do z@c0_Z(~huxrbudpd_%7=2iW?SEeGfHkVEn}=0F0-9}=|RF#qLSwU&DEOCb3=S~b|W zL*en8E=b;sLksre@Pd*KeAj{Rs|cZ^or)=g&VaL1oWf(zQ7x}vzoZaZt|ujJ6M6+) z`z0;tE-enYW0FBoW=fWVW*edfN+)|0dMcglpiUh;H}Ey*)+A(yGLYa4kbxTs*_)705LV(AR8yxG(c93`19={1$vo!q8~-3i*L8hn@62U>-#} z+1zmyUVde|`7g>xik%Tm;=|mOrru-{P7Tq9n|)i8R34MbRb`#RpD+@xEpv3rbd6}Q z4mwLSWtKuU??^d^hn@#?%6S^tzqo6E=wyFZR1G*QTv;xf^`!^eiY$J48l`ny{Gvj%rm^2pC!{ik9xd- zXaomwEnZ|&JkK0>+4d~yk(?g1$ySlr)})WO+Bv|A7q`hVg>5ck$#L?GgUhVdL?a9} zMZu~*ijO%H{?CHr2X*H%1K#=plXbr25dR}jIR86N($r5(aTe0H(WIB`R7n=fl%=Q8 zP@IBx%W_hXG-gUmOd>(2L%C7W&sycj_jrHOfl&=Xcfj2#SCXdmfPMf^DuU^=8J(&( zdbKJ~e$gq{QO^h%2UyG)opO_q%iYYKpeNwTlv`u6X){t)li9Or69Nnp@7?=pstcJd zR|i*>TT_U+8jQmkV79|BW2;BunzK zQ;PUvU6v%rMN$6B1iZ%E<#lA>P5vPH4*Kzjje&TVv3L&?@II#F11w;LR)ddtrGJb| z@d;`2Q>N-?MAzq>{RRJh$-3=pNyEF6g+pBGO>)v<8%|3Z#|v?<=F;2*&ZXFBC#8qB zU(%vFJKbF3X**|il;YC_?gruu&Coo|@;u4ayqZt(tW=)BB3tpRJwpa};n^mn9j6iZ zi88K6mpWZ8;hlI4({)+T#OHH7zm6iC_kT{7`ct?4k4z!S|41!Bga4bGS=X3x8o_bm zT5A6SC{)6( zgwS6JzfK4PmC%t8N|ex<5X#z>SuP!?UHdAeC(>BwWHDUX;*7GEcA=lRS$XLgEBqB! zJeV(DgvG}q&Fn(8t4_F{)t9QUX^AaWVQa+9EX_<(Z5oVh{U|c%WmEmxIquO;)XnK$ z^_pkDYq9>A$myz3N#|>a?6vdhaX#n$`+L_xdn?`Jx;Fc864JFaEgeFWe2+}LzKuy( z^b}{x{vR{-OwttB_atq3lD7Dony~f%-Q813liX9%Vq$lEwap~T<1d$eY{zk3zp-1Q zPwhgAI;nJd!yG^k?Z!S$%d+2AIaBLSQKjlsc(tLFd)Ut$` lLo3qyYsHCHX|h$$)*0aCtA0JZ3%6?H;L$3z3ECtC{tp(+ZB_sP 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 63696f716e57be13574d11a1329454a1e67ff966..95e0077d1cfe0c338550ca8c8bde7d0d40022352 100644 GIT binary patch delta 4075 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 delta 3598 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 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