Kosten Endpunkt
This commit is contained in:
@@ -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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
143
Scripts/KI-Kostenverfolgung.sql
Normal file
143
Scripts/KI-Kostenverfolgung.sql
Normal file
@@ -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;
|
||||
-- /
|
||||
@@ -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;
|
||||
/
|
||||
|
||||
@@ -73,6 +73,11 @@
|
||||
<artifactId>langchain4j-ollama</artifactId>
|
||||
<version>${langchain4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-mistral-ai</artifactId>
|
||||
<version>${langchain4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- Dokumentenverarbeitung -->
|
||||
|
||||
@@ -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() {
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.modelName(mainModelName)
|
||||
if (isMistral()) {
|
||||
return MistralAiChatModel.builder()
|
||||
.apiKey(mistralApiKey)
|
||||
.modelName(mistralMainModel)
|
||||
.temperature(mainTemperature)
|
||||
.timeout(parseDuration(mainTimeout))
|
||||
.maxRetries(mainMaxRetries)
|
||||
.customHeaders(Map.of("X-API-KEY", apiKey))
|
||||
.logRequests(logRequests)
|
||||
.logResponses(logResponses)
|
||||
.build();
|
||||
}
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(ollamaBaseUrl)
|
||||
.modelName(ollamaMainModel)
|
||||
.temperature(mainTemperature)
|
||||
.timeout(parseDuration(mainTimeout))
|
||||
.maxRetries(mainMaxRetries)
|
||||
.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() {
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.modelName(catalogModelName)
|
||||
if (isMistral()) {
|
||||
return MistralAiChatModel.builder()
|
||||
.apiKey(mistralApiKey)
|
||||
.modelName(mistralCatalogModel)
|
||||
.temperature(catalogTemperature)
|
||||
.timeout(parseDuration(catalogTimeout))
|
||||
.maxRetries(catalogMaxRetries)
|
||||
.customHeaders(Map.of("X-API-KEY", apiKey))
|
||||
.logRequests(logRequests)
|
||||
.logResponses(logResponses)
|
||||
.build();
|
||||
}
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(ollamaBaseUrl)
|
||||
.modelName(ollamaCatalogModel)
|
||||
.temperature(catalogTemperature)
|
||||
.timeout(parseDuration(catalogTimeout))
|
||||
.maxRetries(catalogMaxRetries)
|
||||
.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() {
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.modelName(ocrModelName)
|
||||
if (isMistral()) {
|
||||
return MistralAiChatModel.builder()
|
||||
.apiKey(mistralApiKey)
|
||||
.modelName(mistralOcrModel)
|
||||
.timeout(parseDuration(ocrTimeout))
|
||||
.maxRetries(ocrMaxRetries)
|
||||
.customHeaders(Map.of("X-API-KEY", apiKey))
|
||||
.logRequests(logRequests)
|
||||
.logResponses(logResponses)
|
||||
.build();
|
||||
}
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(ollamaBaseUrl)
|
||||
.modelName(ollamaOcrModel)
|
||||
.timeout(parseDuration(ocrTimeout))
|
||||
.maxRetries(ocrMaxRetries)
|
||||
.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", "")));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,9 @@ public class CatalogGenerationService {
|
||||
@Named("catalog")
|
||||
ChatLanguageModel catalogModel;
|
||||
|
||||
@Inject
|
||||
AiCostTrackerService costTracker;
|
||||
|
||||
@Inject
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
@@ -106,7 +109,7 @@ public class CatalogGenerationService {
|
||||
catalogId, chunkNr, totalChunks, chunks.get(i).length());
|
||||
|
||||
GeneratedCatalogStructure chunkResult =
|
||||
generateStructureFromChunk(catalogName, chunks.get(i), chunkNr, totalChunks);
|
||||
generateStructureFromChunk(catalogName, chunks.get(i), chunkNr, totalChunks, catalogId);
|
||||
|
||||
if (chunkResult == null || chunkResult.getCategories() == null) {
|
||||
LOG.warnf("[Katalog %d] Chunk %d lieferte kein Ergebnis – übersprungen",
|
||||
@@ -316,7 +319,8 @@ public class CatalogGenerationService {
|
||||
private GeneratedCatalogStructure generateStructureFromChunk(String catalogName,
|
||||
String chunkText,
|
||||
int chunkNr,
|
||||
int totalChunks) {
|
||||
int totalChunks,
|
||||
long catalogId) {
|
||||
List<ChatMessage> messages = List.of(
|
||||
SystemMessage.from(buildChunkSystemPrompt(chunkNr, totalChunks)),
|
||||
UserMessage.from(
|
||||
@@ -327,6 +331,7 @@ public class CatalogGenerationService {
|
||||
);
|
||||
|
||||
Response<AiMessage> response = catalogModel.generate(messages);
|
||||
costTracker.track("catalog", "CATALOG", null, catalogId, response.tokenUsage());
|
||||
String rawJson = response.content().text();
|
||||
return parseCatalogStructure(rawJson, catalogName + " Chunk " + chunkNr);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -46,6 +46,9 @@ public class DocumentProcessingService {
|
||||
@Named("ocr")
|
||||
ChatLanguageModel ocrModel;
|
||||
|
||||
@Inject
|
||||
AiCostTrackerService costTracker;
|
||||
|
||||
// =========================================================================
|
||||
// Öffentliche API
|
||||
// =========================================================================
|
||||
@@ -59,13 +62,17 @@ public class DocumentProcessingService {
|
||||
* @return Markdown-String
|
||||
*/
|
||||
public String extractText(byte[] fileBytes, String mimeType, String filename) {
|
||||
return extractText(fileBytes, mimeType, filename, null);
|
||||
}
|
||||
|
||||
public String extractText(byte[] fileBytes, String mimeType, String filename, Long projectId) {
|
||||
String effectiveMime = resolveMimeType(mimeType, filename);
|
||||
LOG.debugf("Extrahiere Text: %s (%s)", filename, effectiveMime);
|
||||
|
||||
try {
|
||||
return switch (effectiveMime) {
|
||||
case "application/pdf"
|
||||
-> extractFromPdf(fileBytes);
|
||||
-> extractFromPdf(fileBytes, projectId);
|
||||
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword"
|
||||
-> extractFromDocx(fileBytes);
|
||||
@@ -88,7 +95,7 @@ public class DocumentProcessingService {
|
||||
// PDF: Seiten rendern + KI-OCR
|
||||
// =========================================================================
|
||||
|
||||
private String extractFromPdf(byte[] fileBytes) throws Exception {
|
||||
private String extractFromPdf(byte[] fileBytes, Long projectId) throws Exception {
|
||||
LOG.debug("Starte PDF-OCR...");
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
@@ -101,12 +108,11 @@ public class DocumentProcessingService {
|
||||
LOG.debugf("OCR Seite %d/%d", i + 1, pageCount);
|
||||
BufferedImage image = renderer.renderImageWithDPI(i, OCR_DPI, ImageType.RGB);
|
||||
|
||||
// Bild in PNG-Base64 umwandeln
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "PNG", baos);
|
||||
String base64Image = Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||
|
||||
String pageText = ocrPage(base64Image, i + 1, pageCount);
|
||||
String pageText = ocrPage(base64Image, i + 1, pageCount, projectId);
|
||||
result.append(pageText).append("\n\n");
|
||||
}
|
||||
}
|
||||
@@ -114,10 +120,7 @@ public class DocumentProcessingService {
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet eine PDF-Seite als Base64-Bild an das Vision-Modell.
|
||||
*/
|
||||
private String ocrPage(String base64Image, int pageNum, int totalPages) {
|
||||
private String ocrPage(String base64Image, int pageNum, int totalPages, Long projectId) {
|
||||
UserMessage message = UserMessage.from(
|
||||
ImageContent.from(base64Image, "image/png"),
|
||||
TextContent.from(
|
||||
@@ -134,7 +137,10 @@ public class DocumentProcessingService {
|
||||
)
|
||||
);
|
||||
|
||||
return ocrModel.generate(List.of(message)).content().text();
|
||||
dev.langchain4j.model.output.Response<dev.langchain4j.data.message.AiMessage> response =
|
||||
ocrModel.generate(List.of(message));
|
||||
costTracker.track("ocr", "OCR", projectId, null, response.tokenUsage());
|
||||
return response.content().text();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -39,6 +39,9 @@ public class EvaluationService {
|
||||
@Named("main")
|
||||
ChatLanguageModel mainModel;
|
||||
|
||||
@Inject
|
||||
AiCostTrackerService costTracker;
|
||||
|
||||
@Inject
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
@@ -51,9 +54,10 @@ public class EvaluationService {
|
||||
* Wenn der Text bereits auf Deutsch ist, wird er unverändert zurückgegeben.
|
||||
*
|
||||
* @param originalText Markdown-Text in Originalsprache
|
||||
* @param projectId Für Kostenzuordnung
|
||||
* @return Markdown-Text auf Deutsch
|
||||
*/
|
||||
public String translate(String originalText) {
|
||||
public String translate(String originalText, long projectId) {
|
||||
if (originalText == null || originalText.isBlank()) return "";
|
||||
LOG.debugf("Starte Übersetzung (%d Zeichen)...", originalText.length());
|
||||
|
||||
@@ -71,6 +75,7 @@ public class EvaluationService {
|
||||
);
|
||||
|
||||
Response<AiMessage> response = mainModel.generate(messages);
|
||||
costTracker.track("main", "TRANSLATE", projectId, null, response.tokenUsage());
|
||||
return response.content().text().trim();
|
||||
}
|
||||
|
||||
@@ -88,7 +93,7 @@ public class EvaluationService {
|
||||
* @param documentText Originaltext des Dokuments (Markdown)
|
||||
* @return Strukturiertes Auswertungsergebnis
|
||||
*/
|
||||
public EvaluationResult evaluate(OrdsQuestion question, String documentText) {
|
||||
public EvaluationResult evaluate(OrdsQuestion question, String documentText, long projectId) {
|
||||
LOG.debugf("Werte Frage %d aus: %s",
|
||||
question.getQuestionId(),
|
||||
abbreviate(question.getQuestionText(), 80));
|
||||
@@ -102,6 +107,7 @@ public class EvaluationService {
|
||||
);
|
||||
|
||||
Response<AiMessage> response = mainModel.generate(messages);
|
||||
costTracker.track("main", "EVALUATE", projectId, null, response.tokenUsage());
|
||||
String rawText = response.content().text();
|
||||
|
||||
return parseEvaluationResult(rawText, question);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user