GUI-Bugfixes: Defaults beim Start, kopierbare Meldungen mit Zeitstempel, Befundauflistung, Modell-ComboBox links, effektiver API-Key für Modellabruf
- Blank-Startzustand zeigt jetzt die Standardvorlage (wie nach "Neu"), neue Factory createEmptyStartState für Tests - Meldungsbereich ist per Kontextmenü bzw. Strg+C kopierbar - Jede Meldung trägt ein führendes [HH:mm:ss]-Präfix - Validieren- und Tests-Aktionen akkumulieren Meldungen, automatische Validierung ersetzt still ihre Einträge - Validieren-Meldung listet alle konkreten Befunde einzeln auf - Modell-ComboBox und manuelles Modellfeld sind linksbündig - ApiKeyResolutionPort liefert jetzt den effektiven API-Schlüsselwert (Default + Env-Adapter-Override), so dass der Modellliste-Test in den technischen Tests nicht mehr "API-Schlüssel fehlt" meldet, obwohl er gesetzt ist
This commit is contained in:
+31
@@ -1,5 +1,7 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.validation.editor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
|
||||
|
||||
@@ -22,6 +24,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApi
|
||||
* <p>
|
||||
* Implementierungen dieses Ports liegen im Adapter-Out-Modul.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ApiKeyResolutionPort {
|
||||
|
||||
/**
|
||||
@@ -37,4 +40,32 @@ public interface ApiKeyResolutionPort {
|
||||
* @return der Descriptor für die effektive Schlüsselherkunft; nie {@code null}
|
||||
*/
|
||||
EffectiveApiKeyDescriptor resolve(AiProviderFamily family, String propertyValue);
|
||||
|
||||
/**
|
||||
* Liefert den effektiven API-Schlüssel-Rohwert anhand derselben Vorrangregel wie {@link #resolve}.
|
||||
* <p>
|
||||
* Dient technischen Tests wie dem Modellkatalogabruf, die den tatsächlichen Schlüssel im
|
||||
* HTTP-Header benötigen. Die Herkunft selbst wird nicht mit zurückgegeben – dafür ist
|
||||
* {@link #resolve} zuständig.
|
||||
* <p>
|
||||
* Diese Default-Implementierung deckt den Fall ab, in dem ein Adapter ausschließlich die
|
||||
* Property-Datei kennt: liefert {@link #resolve} {@code ABSENT}, wird ein leerer Optional
|
||||
* zurückgegeben; andernfalls wird der nicht-leere Property-Wert geliefert. Adapter, die
|
||||
* Umgebungsvariablen lesen, müssen diese Methode überschreiben, damit der ENV-Wert auch
|
||||
* tatsächlich an HTTP-Aufrufer durchgereicht wird.
|
||||
*
|
||||
* @param family die Provider-Familie; darf nicht {@code null} sein
|
||||
* @param propertyValue aktueller Property-Wert aus dem Editor (kann leer sein); darf nicht {@code null} sein
|
||||
* @return der effektive Schlüssel-Rohwert, falls eine der Quellen einen Wert liefert; sonst leer
|
||||
*/
|
||||
default Optional<String> resolveEffectiveApiKeyValue(AiProviderFamily family, String propertyValue) {
|
||||
EffectiveApiKeyDescriptor descriptor = resolve(family, propertyValue);
|
||||
if (descriptor.isAbsent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (propertyValue != null && !propertyValue.isBlank()) {
|
||||
return Optional.of(propertyValue);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -28,10 +28,12 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApi
|
||||
* @param claudeModel Rohtextwert des Claude-Modellnamens
|
||||
* @param claudeTimeoutSeconds Rohtextwert des Claude-Timeouts
|
||||
* @param claudeApiKeyDescriptor API-Key-Herkunft für den Claude-Provider; nie {@code null}
|
||||
* @param claudeApiKeyPropertyValue Rohwert des Claude-API-Schlüssels aus der Properties-Datei
|
||||
* @param openaiBaseUrl Rohtextwert der OpenAI-kompatiblen Basis-URL
|
||||
* @param openaiModel Rohtextwert des OpenAI-kompatiblen Modellnamens
|
||||
* @param openaiTimeoutSeconds Rohtextwert des OpenAI-kompatiblen Timeouts
|
||||
* @param openaiApiKeyDescriptor API-Key-Herkunft für den OpenAI-kompatiblen Provider; nie {@code null}
|
||||
* @param openaiApiKeyPropertyValue Rohwert des OpenAI-API-Schlüssels aus der Properties-Datei
|
||||
*/
|
||||
public record EditorValidationInput(
|
||||
String activeProviderIdentifier,
|
||||
@@ -46,10 +48,12 @@ public record EditorValidationInput(
|
||||
String claudeModel,
|
||||
String claudeTimeoutSeconds,
|
||||
EffectiveApiKeyDescriptor claudeApiKeyDescriptor,
|
||||
String claudeApiKeyPropertyValue,
|
||||
String openaiBaseUrl,
|
||||
String openaiModel,
|
||||
String openaiTimeoutSeconds,
|
||||
EffectiveApiKeyDescriptor openaiApiKeyDescriptor) {
|
||||
EffectiveApiKeyDescriptor openaiApiKeyDescriptor,
|
||||
String openaiApiKeyPropertyValue) {
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Eingabe für den Validator.
|
||||
@@ -66,10 +70,12 @@ public record EditorValidationInput(
|
||||
* @param claudeModel Claude-Modellname; {@code null} wird zu leerem String
|
||||
* @param claudeTimeoutSeconds Claude-Timeout; {@code null} wird zu leerem String
|
||||
* @param claudeApiKeyDescriptor Claude-API-Key-Herkunft; darf nicht {@code null} sein
|
||||
* @param claudeApiKeyPropertyValue Claude-API-Key-Rohwert aus der Properties-Datei; {@code null} wird zu leerem String
|
||||
* @param openaiBaseUrl OpenAI-Basis-URL; {@code null} wird zu leerem String
|
||||
* @param openaiModel OpenAI-Modellname; {@code null} wird zu leerem String
|
||||
* @param openaiTimeoutSeconds OpenAI-Timeout; {@code null} wird zu leerem String
|
||||
* @param openaiApiKeyDescriptor OpenAI-API-Key-Herkunft; darf nicht {@code null} sein
|
||||
* @param openaiApiKeyPropertyValue OpenAI-API-Key-Rohwert aus der Properties-Datei; {@code null} wird zu leerem String
|
||||
* @throws NullPointerException wenn {@code claudeApiKeyDescriptor} oder {@code openaiApiKeyDescriptor} {@code null} sind
|
||||
*/
|
||||
public EditorValidationInput {
|
||||
@@ -86,11 +92,13 @@ public record EditorValidationInput(
|
||||
claudeTimeoutSeconds = normalizeText(claudeTimeoutSeconds);
|
||||
claudeApiKeyDescriptor = Objects.requireNonNull(claudeApiKeyDescriptor,
|
||||
"claudeApiKeyDescriptor must not be null");
|
||||
claudeApiKeyPropertyValue = normalizeText(claudeApiKeyPropertyValue);
|
||||
openaiBaseUrl = normalizeText(openaiBaseUrl);
|
||||
openaiModel = normalizeText(openaiModel);
|
||||
openaiTimeoutSeconds = normalizeText(openaiTimeoutSeconds);
|
||||
openaiApiKeyDescriptor = Objects.requireNonNull(openaiApiKeyDescriptor,
|
||||
"openaiApiKeyDescriptor must not be null");
|
||||
openaiApiKeyPropertyValue = normalizeText(openaiApiKeyPropertyValue);
|
||||
}
|
||||
|
||||
private static String normalizeText(String value) {
|
||||
|
||||
+25
-14
@@ -79,6 +79,8 @@ public class ProviderTechnicalTestService {
|
||||
private static final int DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
|
||||
private final AiModelCatalogPort modelCatalogPort;
|
||||
private final ApiKeyResolutionPort apiKeyResolutionPort;
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Service mit den erforderlichen Ports.
|
||||
*
|
||||
@@ -89,7 +91,7 @@ public class ProviderTechnicalTestService {
|
||||
public ProviderTechnicalTestService(AiModelCatalogPort modelCatalogPort,
|
||||
ApiKeyResolutionPort apiKeyResolutionPort) {
|
||||
this.modelCatalogPort = Objects.requireNonNull(modelCatalogPort, "modelCatalogPort must not be null");
|
||||
Objects.requireNonNull(apiKeyResolutionPort,
|
||||
this.apiKeyResolutionPort = Objects.requireNonNull(apiKeyResolutionPort,
|
||||
"apiKeyResolutionPort must not be null");
|
||||
}
|
||||
|
||||
@@ -377,13 +379,10 @@ public class ProviderTechnicalTestService {
|
||||
/**
|
||||
* Baut den {@link ModelCatalogRequest} aus dem aktuellen Editorzustand auf.
|
||||
* <p>
|
||||
* Da {@link EditorValidationInput} keinen direkten API-Key-String enthält, sondern
|
||||
* nur einen bereits aufgelösten {@link EffectiveApiKeyDescriptor}, wird der Descriptor
|
||||
* aus dem Editorzustand direkt verwendet. Der Adapter-Out-Seitige Dispatcher erwartet
|
||||
* den Key entweder als ENV-Variable (die er selbst liest) oder als optionalen Wert
|
||||
* im Request. Da die Auflösung beim Service bereits über {@link ApiKeyResolutionPort}
|
||||
* erfolgt ist, wird für den Catalog-Request ein leerer Optional-Wert geliefert –
|
||||
* der Adapter verwendet dann intern seine eigene ENV-Variable-Auflösung.
|
||||
* Der effektive API-Key-Rohwert wird über den {@link ApiKeyResolutionPort} ermittelt
|
||||
* (Vorrangregel: providerspezifische ENV → Legacy-ENV → Property-Wert) und in den
|
||||
* Request übernommen. Dadurch ist der Schlüssel bereits beim Adapter verfügbar und
|
||||
* spiegelt exakt die Quelle wider, die zuvor im Deskriptor ausgewiesen wurde.
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @param family aktive Provider-Familie
|
||||
@@ -393,12 +392,9 @@ public class ProviderTechnicalTestService {
|
||||
private ModelCatalogRequest buildCatalogRequest(EditorValidationInput input,
|
||||
AiProviderFamily family,
|
||||
EffectiveApiKeyDescriptor apiKeyDesc) {
|
||||
// EditorValidationInput enthält keinen direkten API-Key-String-Wert, nur den Descriptor.
|
||||
// Für den ModelCatalogRequest übergeben wir einen leeren Optional für den apiKey,
|
||||
// sodass der Adapter seine eigene ENV-Variable-Auflösung durchführt.
|
||||
// Der Adapter liefert dann IncompleteConfiguration, wenn auch er keinen Key findet –
|
||||
// was aber nicht passiert, da wir oben bereits geprüft haben, dass apiKeyDesc nicht ABSENT ist.
|
||||
Optional<String> apiKeyForRequest = Optional.empty();
|
||||
String propertyValue = resolveApiKeyPropertyValue(input, family);
|
||||
Optional<String> apiKeyForRequest = apiKeyResolutionPort
|
||||
.resolveEffectiveApiKeyValue(family, propertyValue);
|
||||
|
||||
String rawBaseUrl = resolveBaseUrlValue(input, family);
|
||||
Optional<String> baseUrl = rawBaseUrl.isBlank() ? Optional.empty() : Optional.of(rawBaseUrl);
|
||||
@@ -412,6 +408,21 @@ public class ProviderTechnicalTestService {
|
||||
timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest den Roh-Property-Wert des API-Schlüssels für die angegebene Provider-Familie
|
||||
* aus dem Editorzustand.
|
||||
*
|
||||
* @param input aktueller Editorzustand
|
||||
* @param family aktive Provider-Familie
|
||||
* @return Property-Wert; nie {@code null}, leer wenn nicht gesetzt
|
||||
*/
|
||||
private String resolveApiKeyPropertyValue(EditorValidationInput input, AiProviderFamily family) {
|
||||
return switch (family) {
|
||||
case CLAUDE -> input.claudeApiKeyPropertyValue();
|
||||
case OPENAI_COMPATIBLE -> input.openaiApiKeyPropertyValue();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest den bereits aufgelösten {@link EffectiveApiKeyDescriptor} für die aktive Provider-Familie
|
||||
* direkt aus dem {@link EditorValidationInput}.
|
||||
|
||||
Reference in New Issue
Block a user