feat: AP-A Token-Tracking Fundament - Schema, Adapter, Use Cases, GUI (#74)
Erste Stufe der V3.3-Spezifikation: Token- und Kosten-Tracking-Fundament. Schema und Persistenz: - Neue Flyway-Migration V2__token_tracking.sql mit sechs Token-/Preis-Snapshot- Spalten in processing_attempt, neuer model_price-Tabelle (Composite-Key provider+model_name) und Default-Preisen fuer beide Provider-Familien. - SqliteModelPriceRepositoryAdapter mit UPSERT, transaktionalem Batch und invalidUpdatedAt-Mapping. - Zentrale SqliteConnectionFactory; alle direkten DriverManager.getConnection- Stellen in den Repository-Adaptern (Document, Attempt, History, UnitOfWork) auf die Factory umgezogen, damit WAL und busy_timeout pro Connection greifen. Application und Domain: - Neue DTOs AiUsageMetadata, ModelPriceEntry/View/Key/ChangeSet, CostResult. - AiInvocationSuccess um usageMetadata erweitert; AiAttemptContext um vier nullable Token-Felder. - ProcessingAttempt um sechs Token-/Preis-Snapshot-Felder erweitert (Convenience-Konstruktor und withoutAiFields-Factory unveraendert). - ModelPriceRepository-Port mit Schreib-/Lese-Trennung. - DefaultManageModelPricesUseCase mit ChangeSet-Konfliktvalidierung, Provider-Whitelist und Clock-Stempel. - CostCalculator (formatRow + calculateAttempt; formatTotal als Stub fuer AP-B). KI-Adapter: - AnthropicClaudeHttpAdapter und OpenAiHttpAdapter extrahieren Token-Daten aus den Response-Bodies inklusive Validierung (negativ, > 10 Mio., nicht numerisch -> NULL + WARN-Log). BatchRunProcessingUseCase-Hook: - DocumentProcessingCoordinator erhaelt optional ModelPriceRepository und ein Headless-Flag. Beim Bau eines KI-Versuchs wird der Snapshot-Preis fuer (Provider, Modell) geladen und mit den Token-Daten am ProcessingAttempt persistiert. Lookup-Fehler verlieren keinen Attempt. GUI: - Neuer Tab "Modell-Preise" (TableView mit Editierfeldern, Add-Dialog, Loesch-Bestaetigung, Konvertierung Nano-USD <-> $/1M Tokens). - History-Tab um drei Spalten erweitert: Input-Tokens, Output-Tokens, Kosten. - Summary-Banner um Token-, Kosten- und Cache-only-Zeile erweitert (Default-Werte; AP-B liefert spaeter die echten Aggregate). - Konfigurations-Tab warnt beim Speichern, wenn das aktive Modell keinen Preis-Eintrag hat. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+80
-6
@@ -454,7 +454,8 @@ public class BootstrapRunner {
|
||||
this.useCaseFactory = (startConfig, lock) -> buildProductionBatchUseCase(
|
||||
startConfig, lock,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver.noOp(),
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken.neverCancelled());
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken.neverCancelled(),
|
||||
true);
|
||||
this.commandFactory = SchedulerBatchCommand::new;
|
||||
this.guiAdapterFactory = GuiAdapter::new;
|
||||
this.singleInstanceGuardFactory = SingleInstanceGuard::new;
|
||||
@@ -479,6 +480,23 @@ public class BootstrapRunner {
|
||||
RunLockPort runLockPort,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken cancellationToken) {
|
||||
return buildProductionBatchUseCase(
|
||||
startConfig, runLockPort, progressObserver, cancellationToken, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erweiterte Variante mit Headless-Modus-Flag fuer den Token-Tracking-Hook.
|
||||
*
|
||||
* @param headlessMode {@code true} fuer headless Laufkontext; aktiviert den
|
||||
* CLI-Hinweis bei fehlendem Modell-Preis-Eintrag
|
||||
* @return verdrahteter Use Case
|
||||
*/
|
||||
private BatchRunProcessingUseCase buildProductionBatchUseCase(
|
||||
StartConfiguration startConfig,
|
||||
RunLockPort runLockPort,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver progressObserver,
|
||||
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken cancellationToken,
|
||||
boolean headlessMode) {
|
||||
AiContentSensitivity aiContentSensitivity = resolveAiContentSensitivity(startConfig.logAiSensitive());
|
||||
RuntimeConfiguration runtimeConfig = new RuntimeConfiguration(
|
||||
startConfig.maxPages(), startConfig.maxRetriesTransient(), aiContentSensitivity);
|
||||
@@ -499,12 +517,16 @@ public class BootstrapRunner {
|
||||
DocumentProcessingCoordinator.class, aiContentSensitivity);
|
||||
TargetFolderPort targetFolderPort = new FilesystemTargetFolderAdapter(startConfig.targetFolder());
|
||||
TargetFileCopyPort targetFileCopyPort = new FilesystemTargetFileCopyAdapter(startConfig.targetFolder());
|
||||
de.gecheckt.pdf.umbenenner.application.port.out.ModelPriceRepository modelPriceRepository =
|
||||
new de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteModelPriceRepositoryAdapter(jdbcUrl);
|
||||
DocumentProcessingCoordinator documentProcessingCoordinator =
|
||||
new DocumentProcessingCoordinator(documentRecordRepository, processingAttemptRepository,
|
||||
unitOfWorkPort, targetFolderPort, targetFileCopyPort, coordinatorLogger,
|
||||
startConfig.maxRetriesTransient(),
|
||||
startConfig.maxTitleLength(),
|
||||
activeFamily.getIdentifier());
|
||||
activeFamily.getIdentifier(),
|
||||
modelPriceRepository,
|
||||
headlessMode);
|
||||
|
||||
PromptPort promptPort = new FilesystemPromptPortAdapter(startConfig.promptTemplateFile());
|
||||
ClockPort clockPort = new SystemClockAdapter();
|
||||
@@ -916,6 +938,8 @@ public class BootstrapRunner {
|
||||
GuiManualFileCopyPort manualCopyPort = this::performGuiManualFileCopy;
|
||||
GuiHistoricalDocumentContextPort historicalDocumentContextPort = this::resolveHistoricalDocumentContextForGui;
|
||||
GuiHistoryOverviewPort historyOverviewPort = this::loadHistoryOverviewForGui;
|
||||
de.gecheckt.pdf.umbenenner.adapter.in.gui.modelprices.GuiModelPriceManagementPort modelPricePort =
|
||||
buildGuiModelPriceManagementPort();
|
||||
GuiHistoryDetailsPort historyDetailsPort = this::loadHistoryDetailsForGui;
|
||||
GuiCreateNewDatabasePort createNewDatabasePort = this::createNewDatabaseForGui;
|
||||
GuiHistoryResetDocumentStatusPort historyResetPort = this::resetHistoryDocumentStatusForGui;
|
||||
@@ -966,7 +990,8 @@ public class BootstrapRunner {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
contextInitializer);
|
||||
contextInitializer,
|
||||
Optional.of(modelPricePort));
|
||||
}
|
||||
|
||||
Path configPath = Paths.get(configPathOverride.get());
|
||||
@@ -1002,7 +1027,8 @@ public class BootstrapRunner {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
contextInitializer);
|
||||
contextInitializer,
|
||||
Optional.of(modelPricePort));
|
||||
}
|
||||
|
||||
LOG.info("GUI startup: configuration file confirmed at: {}", configPath.toAbsolutePath());
|
||||
@@ -1025,7 +1051,8 @@ public class BootstrapRunner {
|
||||
historicalDocumentContextPort, applicationVersion, promptEditorPort,
|
||||
historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort,
|
||||
this::buildGuiPromptEditorPort, createNewDatabasePort, contextError,
|
||||
schedulerUseCase, guiRunLockPort, contextInitializer);
|
||||
schedulerUseCase, guiRunLockPort, contextInitializer,
|
||||
Optional.of(modelPricePort));
|
||||
} catch (GuiConfigurationLoadException e) {
|
||||
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
|
||||
e.getMessage(), e);
|
||||
@@ -1057,7 +1084,8 @@ public class BootstrapRunner {
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
contextInitializer);
|
||||
contextInitializer,
|
||||
Optional.of(modelPricePort));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2169,6 +2197,52 @@ public class BootstrapRunner {
|
||||
migrationStep.runIfNeeded(effectiveConfigPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine GUI-Bridge-Implementierung fuer den Modell-Preis-Tab.
|
||||
*
|
||||
* <p>Pro Methodenaufruf wird ein frischer SQLite-Adapter aus der zur
|
||||
* Konfigurationsdatei gehoerenden JDBC-URL aufgebaut. Die Methoden des
|
||||
* Ports laufen auf einem GUI-Worker-Thread; die Connections werden
|
||||
* try-with-resources sofort wieder geschlossen.
|
||||
*
|
||||
* @return Bridge-Port-Implementierung; nie {@code null}
|
||||
*/
|
||||
private de.gecheckt.pdf.umbenenner.adapter.in.gui.modelprices.GuiModelPriceManagementPort
|
||||
buildGuiModelPriceManagementPort() {
|
||||
return new de.gecheckt.pdf.umbenenner.adapter.in.gui.modelprices.GuiModelPriceManagementPort() {
|
||||
@Override
|
||||
public java.util.List<de.gecheckt.pdf.umbenenner.application.dto.ModelPriceView>
|
||||
findAll(Path configFilePath) {
|
||||
Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
|
||||
String jdbcUrl = resolveJdbcUrlForGui(configFilePath);
|
||||
return new de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteModelPriceRepositoryAdapter(jdbcUrl)
|
||||
.findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<de.gecheckt.pdf.umbenenner.application.dto.ModelPriceView>
|
||||
findByProviderAndModelName(Path configFilePath, String provider, String modelName) {
|
||||
Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
|
||||
String jdbcUrl = resolveJdbcUrlForGui(configFilePath);
|
||||
return new de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteModelPriceRepositoryAdapter(jdbcUrl)
|
||||
.findByProviderAndModelName(provider, modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAllChanges(Path configFilePath,
|
||||
de.gecheckt.pdf.umbenenner.application.dto.ModelPriceChangeSet changeSet) {
|
||||
Objects.requireNonNull(configFilePath, CONFIG_FILE_NOT_NULL);
|
||||
String jdbcUrl = resolveJdbcUrlForGui(configFilePath);
|
||||
de.gecheckt.pdf.umbenenner.application.port.out.ModelPriceRepository repository =
|
||||
new de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteModelPriceRepositoryAdapter(jdbcUrl);
|
||||
de.gecheckt.pdf.umbenenner.application.usecase.DefaultManageModelPricesUseCase useCase =
|
||||
new de.gecheckt.pdf.umbenenner.application.usecase.DefaultManageModelPricesUseCase(
|
||||
repository, new SystemClockAdapter());
|
||||
useCase.saveAllChanges(changeSet);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die aktive JDBC-URL für GUI-Aufrufe, die keinen vollständigen
|
||||
* {@link StartConfiguration} benötigen.
|
||||
|
||||
+2
-1
@@ -104,6 +104,7 @@ final class StubAiInvocationPort implements AiInvocationPort {
|
||||
+ "\"title\": \"" + title + "\", "
|
||||
+ "\"reasoning\": \"" + reasoning + "\""
|
||||
+ "}";
|
||||
return new AiInvocationSuccess(request, new AiRawResponse(rawJson));
|
||||
return new AiInvocationSuccess(request, new AiRawResponse(rawJson),
|
||||
de.gecheckt.pdf.umbenenner.application.dto.AiUsageMetadata.empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user