AiTargetCatalogService.java
package com.taxonomy.analysis.service;
import com.taxonomy.analysis.dto.AiTargetDtos.AiTargetCatalogView;
import com.taxonomy.analysis.dto.AiTargetDtos.AiTargetDescriptor;
import com.taxonomy.analysis.dto.AiTargetDtos.AiTargetHealth;
import com.taxonomy.analysis.dto.AiTargetDtos.AiTargetMode;
import com.taxonomy.analysis.dto.AiTargetDtos.PromptBudget;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
/**
* Resolves stable, non-secret identities for configured AI endpoints and models.
* Credentials and raw custom endpoint URLs never leave this component.
*/
@Component
public class AiTargetCatalogService {
private static final String MOCK_MODEL = "taxonomy-deterministic-v1";
private static final String LOCAL_MODEL = "bge-small-en-v1.5";
private static final String GEMINI_MODEL = "gemini-3-flash-preview";
private static final PromptBudget DEFAULT_PROMPT_BUDGET =
new PromptBudget(120_000, 262_144, 30_000);
private final LlmProviderConfig providerConfig;
private final PromptBudget promptBudget;
public AiTargetCatalogService(LlmProviderConfig providerConfig) {
this.providerConfig = providerConfig;
this.promptBudget = DEFAULT_PROMPT_BUDGET;
}
public AiTargetCatalogView catalog() {
AiTargetDescriptor active = activeTarget();
Map<String, AiTargetDescriptor> byId = new LinkedHashMap<>();
byId.put(active.targetId(), active);
if (providerConfig.isMockMode()) {
AiTargetDescriptor mock = mockTarget();
byId.put(mock.targetId(), mock);
}
LlmProvider activeProvider = providerConfig.getActiveProvider();
for (LlmProvider provider : LlmProvider.values()) {
AiTargetDescriptor descriptor = describe(provider);
if (descriptor.available() || provider == activeProvider) {
byId.put(descriptor.targetId(), descriptor);
}
}
List<AiTargetDescriptor> targets = new ArrayList<>(byId.values());
targets.sort(Comparator
.comparing((AiTargetDescriptor target) ->
!Objects.equals(target.targetId(), active.targetId()))
.thenComparing(AiTargetDescriptor::displayName));
return new AiTargetCatalogView(active, List.copyOf(targets));
}
public AiTargetDescriptor activeTarget() {
if (providerConfig.isMockMode()) return mockTarget();
LlmProvider provider = providerConfig.getActiveProvider();
if (provider == null) {
throw new IllegalStateException("No active AI provider is configured");
}
return describe(provider);
}
public AiTargetDescriptor resolve(String targetId, String legacyProvider) {
String requestedTarget = normalize(targetId);
if (requestedTarget != null) {
return catalog().targets().stream()
.filter(candidate -> candidate.targetId().equals(requestedTarget))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"Unknown AI target: " + requestedTarget));
}
String requestedProvider = normalize(legacyProvider);
if (requestedProvider != null) {
if ("MOCK".equalsIgnoreCase(requestedProvider)) {
return mockTarget();
}
try {
return describe(LlmProvider.valueOf(
requestedProvider.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException(
"Unknown AI provider: " + requestedProvider, exception);
}
}
return activeTarget();
}
public AiTargetDescriptor describeProvider(String provider) {
return resolve(null, provider);
}
private AiTargetDescriptor mockTarget() {
boolean available = providerConfig.isMockMode();
return descriptor(
"MOCK",
MOCK_MODEL,
AiTargetMode.MOCK,
available,
available ? null : "MOCK is available only when llm.mock=true.",
"mock");
}
private AiTargetDescriptor describe(LlmProvider provider) {
boolean available = providerConfig.isProviderConfigured(provider);
String reason = available ? null : providerConfig.getProviderConfigurationError(provider);
AiTargetMode mode = provider == LlmProvider.LOCAL_ONNX
? AiTargetMode.LOCAL : AiTargetMode.REMOTE;
return descriptor(
provider.name(),
model(provider),
mode,
available,
reason,
endpointIdentity(provider));
}
private AiTargetDescriptor descriptor(
String provider,
String model,
AiTargetMode mode,
boolean available,
String unavailableReason,
String endpointIdentity) {
String normalizedProvider = provider.toUpperCase(Locale.ROOT);
String safeModel = model == null || model.isBlank() ? "unspecified" : model.strip();
String safeEndpoint = endpointIdentity == null ? "" : endpointIdentity;
String fingerprintMaterial = String.join("\n",
normalizedProvider,
safeModel,
mode.name(),
safeEndpoint,
Integer.toString(promptBudget.maxInputCharacters()),
Integer.toString(promptBudget.maxInputBytes()),
Integer.toString(promptBudget.estimatedMaxInputTokens()));
String fingerprint = sha256(fingerprintMaterial);
String id = normalizedProvider.toLowerCase(Locale.ROOT).replace('_', '-')
+ ":" + slug(safeModel)
+ ("CUSTOM_OPENAI".equals(normalizedProvider)
? ":" + fingerprint.substring(0, 12) : "");
String display = displayProvider(normalizedProvider) + " / " + safeModel;
return new AiTargetDescriptor(
id,
display,
normalizedProvider,
safeModel,
mode,
available ? AiTargetHealth.READY : AiTargetHealth.UNAVAILABLE,
available,
false,
false,
promptBudget,
fingerprint,
unavailableReason);
}
private String model(LlmProvider provider) {
return switch (provider) {
case GEMINI -> GEMINI_MODEL;
case LOCAL_ONNX -> LOCAL_MODEL;
case OPENAI, DEEPSEEK, QWEN, LLAMA, MISTRAL, CUSTOM_OPENAI ->
providerConfig.getOpenAiCompatibleModel(provider);
};
}
private String endpointIdentity(LlmProvider provider) {
return switch (provider) {
case GEMINI -> withoutTrailingEmptyGeminiKey(providerConfig.getGeminiUrl());
case LOCAL_ONNX -> "local:" + LOCAL_MODEL;
case OPENAI, DEEPSEEK, QWEN, LLAMA, MISTRAL, CUSTOM_OPENAI ->
providerConfig.getOpenAiCompatibleUrl(provider);
};
}
private static String withoutTrailingEmptyGeminiKey(String url) {
return url == null ? "" : url.replaceAll("\\?key=$", "");
}
private static String displayProvider(String provider) {
return switch (provider) {
case "MOCK" -> "Mock";
case "GEMINI" -> "Gemini";
case "OPENAI" -> "OpenAI";
case "DEEPSEEK" -> "DeepSeek";
case "QWEN" -> "Qwen";
case "LLAMA" -> "Llama";
case "MISTRAL" -> "Mistral";
case "CUSTOM_OPENAI" -> "Custom OpenAI-compatible";
case "LOCAL_ONNX" -> "Local ONNX";
default -> provider;
};
}
private static String normalize(String value) {
return value == null || value.isBlank() ? null : value.strip();
}
private static String slug(String value) {
String slug = value.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9._-]+", "-")
.replaceAll("^-+|-+$", "");
return slug.isBlank() ? "unspecified" : slug;
}
private static String sha256(String value) {
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(value.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException impossible) {
throw new IllegalStateException("SHA-256 is unavailable", impossible);
}
}
}