| 1 | package org.egothor.methodatlas.ai; | |
| 2 | ||
| 3 | /** | |
| 4 | * Factory responsible for creating provider-specific AI client implementations. | |
| 5 | * | |
| 6 | * <p> | |
| 7 | * This class centralizes the logic for selecting and constructing concrete | |
| 8 | * {@link AiProviderClient} implementations based on the configuration provided | |
| 9 | * through {@link AiOptions}. It abstracts provider instantiation from the rest | |
| 10 | * of the application so that higher-level components interact only with the | |
| 11 | * {@link AiProviderClient} interface. | |
| 12 | * </p> | |
| 13 | * | |
| 14 | * <h2>Provider Resolution</h2> | |
| 15 | * | |
| 16 | * <p> | |
| 17 | * When an explicit provider is configured in {@link AiOptions#provider()}, the | |
| 18 | * factory constructs the corresponding client implementation. When | |
| 19 | * {@link AiProvider#AUTO} is selected, the factory attempts to determine a | |
| 20 | * suitable provider automatically using the following strategy: | |
| 21 | * </p> | |
| 22 | * | |
| 23 | * <ol> | |
| 24 | * <li>Attempt to use a locally running {@link OllamaClient}.</li> | |
| 25 | * <li>If Ollama is not reachable and an API key is configured, fall back to an | |
| 26 | * OpenAI-compatible provider.</li> | |
| 27 | * <li>If no provider can be resolved, an {@link AiSuggestionException} is | |
| 28 | * thrown.</li> | |
| 29 | * </ol> | |
| 30 | * | |
| 31 | * <h2>Azure OpenAI</h2> | |
| 32 | * | |
| 33 | * <p> | |
| 34 | * {@link AiProvider#AZURE_OPENAI} constructs an {@link AzureOpenAiClient}. | |
| 35 | * This provider is never selected automatically; it must be configured | |
| 36 | * explicitly. The {@link AiOptions#baseUrl()} must point to the Azure OpenAI | |
| 37 | * resource endpoint and {@link AiOptions#modelName()} must match the | |
| 38 | * deployment name as shown in the Azure portal. | |
| 39 | * </p> | |
| 40 | * | |
| 41 | * <p> | |
| 42 | * The factory ensures that returned clients are usable by verifying provider | |
| 43 | * availability when required. | |
| 44 | * </p> | |
| 45 | * | |
| 46 | * <p> | |
| 47 | * This class is intentionally non-instantiable and exposes only static factory | |
| 48 | * methods. | |
| 49 | * </p> | |
| 50 | * | |
| 51 | * @see AiProviderClient | |
| 52 | * @see AiProvider | |
| 53 | * @see AiOptions | |
| 54 | */ | |
| 55 | public final class AiProviderFactory { | |
| 56 | /** | |
| 57 | * Prevents instantiation of this utility class. | |
| 58 | */ | |
| 59 | private AiProviderFactory() { | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Creates a provider-specific {@link AiProviderClient} based on the supplied | |
| 64 | * configuration. | |
| 65 | * | |
| 66 | * <p>Rate-limit events are silently discarded. Use | |
| 67 | * {@link #create(AiOptions, RateLimitListener)} when the caller needs to | |
| 68 | * be informed of HTTP 429 pauses.</p> | |
| 69 | * | |
| 70 | * <p> | |
| 71 | * The selected provider determines which concrete implementation is | |
| 72 | * instantiated and how availability checks are performed. When | |
| 73 | * {@link AiProvider#AUTO} is configured, the method delegates provider | |
| 74 | * selection to {@link #auto(AiOptions, RateLimitListener)}. | |
| 75 | * </p> | |
| 76 | * | |
| 77 | * @param options AI configuration describing provider, model, endpoint, | |
| 78 | * authentication, and runtime limits | |
| 79 | * @return initialized provider client ready to perform inference requests | |
| 80 | * | |
| 81 | * @throws AiSuggestionException if the provider cannot be initialized, required | |
| 82 | * authentication is missing, or no suitable | |
| 83 | * provider can be resolved | |
| 84 | * @see #create(AiOptions, RateLimitListener) | |
| 85 | */ | |
| 86 | public static AiProviderClient create(AiOptions options) throws AiSuggestionException { | |
| 87 |
1
1. create : replaced return value with null for org/egothor/methodatlas/ai/AiProviderFactory::create → KILLED |
return create(options, (w, a, m) -> {}); |
| 88 | } | |
| 89 | ||
| 90 | /** | |
| 91 | * Creates a provider-specific {@link AiProviderClient} based on the supplied | |
| 92 | * configuration, notifying {@code rateLimitListener} before each | |
| 93 | * HTTP 429 pause. | |
| 94 | * | |
| 95 | * <p> | |
| 96 | * The selected provider determines which concrete implementation is | |
| 97 | * instantiated and how availability checks are performed. When | |
| 98 | * {@link AiProvider#AUTO} is configured, the method delegates provider | |
| 99 | * selection to {@link #auto(AiOptions, RateLimitListener)}. | |
| 100 | * </p> | |
| 101 | * | |
| 102 | * @param options AI configuration describing provider, model, | |
| 103 | * endpoint, authentication, and runtime limits | |
| 104 | * @param rateLimitListener callback invoked before each rate-limit sleep; | |
| 105 | * must not be {@code null} | |
| 106 | * @return initialized provider client ready to perform inference requests | |
| 107 | * | |
| 108 | * @throws AiSuggestionException if the provider cannot be initialized, required | |
| 109 | * authentication is missing, or no suitable | |
| 110 | * provider can be resolved | |
| 111 | * @see RateLimitListener | |
| 112 | */ | |
| 113 | public static AiProviderClient create(AiOptions options, RateLimitListener rateLimitListener) | |
| 114 | throws AiSuggestionException { | |
| 115 |
2
1. create : Changed switch default to be first case → KILLED 2. create : replaced return value with null for org/egothor/methodatlas/ai/AiProviderFactory::create → KILLED |
return switch (options.provider()) { |
| 116 | case OLLAMA -> new OllamaClient(options, rateLimitListener); | |
| 117 | case OPENAI -> requireAvailable(new OpenAiCompatibleClient(options, rateLimitListener), "OpenAI API key missing"); | |
| 118 | case OPENROUTER -> requireAvailable(new OpenAiCompatibleClient(options, rateLimitListener), "OpenRouter API key missing"); | |
| 119 | case ANTHROPIC -> requireAvailable(new AnthropicClient(options, rateLimitListener), "Anthropic API key missing"); | |
| 120 | case AZURE_OPENAI -> requireAvailable(new AzureOpenAiClient(options, rateLimitListener), "Azure OpenAI API key missing"); | |
| 121 | case GROQ -> requireAvailable(new OpenAiCompatibleClient(options, rateLimitListener), "Groq API key missing"); | |
| 122 | case XAI -> requireAvailable(new OpenAiCompatibleClient(options, rateLimitListener), "xAI API key missing"); | |
| 123 | case GITHUB_MODELS -> requireAvailable(new OpenAiCompatibleClient(options, rateLimitListener), "GitHub token missing"); | |
| 124 | case MISTRAL -> requireAvailable(new OpenAiCompatibleClient(options, rateLimitListener), "Mistral API key missing"); | |
| 125 | case AUTO -> auto(options, rateLimitListener); | |
| 126 | }; | |
| 127 | } | |
| 128 | ||
| 129 | /** | |
| 130 | * Performs automatic provider discovery when {@link AiProvider#AUTO} is | |
| 131 | * selected. | |
| 132 | * | |
| 133 | * <p> | |
| 134 | * The discovery process prioritizes locally available inference services to | |
| 135 | * enable operation without external dependencies whenever possible. | |
| 136 | * </p> | |
| 137 | * | |
| 138 | * <p> | |
| 139 | * The current discovery strategy is: | |
| 140 | * </p> | |
| 141 | * <ol> | |
| 142 | * <li>Attempt to connect to a local {@link OllamaClient}.</li> | |
| 143 | * <li>If Ollama is not available but an API key is configured, create an | |
| 144 | * {@link OpenAiCompatibleClient}.</li> | |
| 145 | * <li>If neither provider can be used, throw an exception.</li> | |
| 146 | * </ol> | |
| 147 | * | |
| 148 | * @param options AI configuration used to construct candidate | |
| 149 | * providers | |
| 150 | * @param rateLimitListener callback threaded through to the resolved | |
| 151 | * provider client; must not be {@code null} | |
| 152 | * @return resolved provider client | |
| 153 | * | |
| 154 | * @throws AiSuggestionException if no suitable provider can be discovered | |
| 155 | */ | |
| 156 | private static AiProviderClient auto(AiOptions options, RateLimitListener rateLimitListener) | |
| 157 | throws AiSuggestionException { | |
| 158 | AiOptions ollamaOptions = AiOptions.builder() | |
| 159 | .enabled(options.enabled()) | |
| 160 | .provider(AiProvider.OLLAMA) | |
| 161 | .modelName(options.modelName()) | |
| 162 | .baseUrl(options.baseUrl()) | |
| 163 | .apiKey(options.apiKey()) | |
| 164 | .apiKeyEnv(options.apiKeyEnv()) | |
| 165 | .taxonomyFile(options.taxonomyFile()) | |
| 166 | .taxonomyMode(options.taxonomyMode()) | |
| 167 | .maxClassChars(options.maxClassChars()) | |
| 168 | .timeout(options.timeout()) | |
| 169 | .maxRetries(options.maxRetries()) | |
| 170 | .build(); | |
| 171 | ||
| 172 | OllamaClient ollamaClient = new OllamaClient(ollamaOptions, rateLimitListener); | |
| 173 |
2
1. auto : removed conditional - replaced equality check with false → KILLED 2. auto : removed conditional - replaced equality check with true → KILLED |
if (ollamaClient.isAvailable()) { |
| 174 |
1
1. auto : replaced return value with null for org/egothor/methodatlas/ai/AiProviderFactory::auto → KILLED |
return ollamaClient; |
| 175 | } | |
| 176 | ||
| 177 | String apiKey = options.resolvedApiKey(); | |
| 178 |
4
1. auto : removed conditional - replaced equality check with true → SURVIVED 2. auto : removed conditional - replaced equality check with true → KILLED 3. auto : removed conditional - replaced equality check with false → KILLED 4. auto : removed conditional - replaced equality check with false → KILLED |
if (apiKey != null && !apiKey.isBlank()) { |
| 179 |
1
1. auto : replaced return value with null for org/egothor/methodatlas/ai/AiProviderFactory::auto → KILLED |
return new OpenAiCompatibleClient(options, rateLimitListener); |
| 180 | } | |
| 181 | ||
| 182 | throw new AiSuggestionException( | |
| 183 | "No AI provider available. Ollama is not reachable and no API key is configured."); | |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * Ensures that a provider client is available before returning it. | |
| 188 | * | |
| 189 | * <p> | |
| 190 | * This helper method invokes {@link AiProviderClient#isAvailable()} and throws | |
| 191 | * an {@link AiSuggestionException} if the provider cannot be used. It is | |
| 192 | * primarily used when constructing clients that require external services or | |
| 193 | * authentication to function correctly. | |
| 194 | * </p> | |
| 195 | * | |
| 196 | * @param client provider client to verify | |
| 197 | * @param message error message used if the provider is unavailable | |
| 198 | * @return the supplied client if it is available | |
| 199 | * | |
| 200 | * @throws AiSuggestionException if the provider reports that it is not | |
| 201 | * available | |
| 202 | */ | |
| 203 | private static AiProviderClient requireAvailable(AiProviderClient client, String message) | |
| 204 | throws AiSuggestionException { | |
| 205 |
2
1. requireAvailable : removed conditional - replaced equality check with true → KILLED 2. requireAvailable : removed conditional - replaced equality check with false → KILLED |
if (!client.isAvailable()) { |
| 206 | throw new AiSuggestionException(message); | |
| 207 | } | |
| 208 |
1
1. requireAvailable : replaced return value with null for org/egothor/methodatlas/ai/AiProviderFactory::requireAvailable → KILLED |
return client; |
| 209 | } | |
| 210 | } | |
Mutations | ||
| 87 |
1.1 |
|
| 115 |
1.1 2.2 |
|
| 173 |
1.1 2.2 |
|
| 174 |
1.1 |
|
| 178 |
1.1 2.2 3.3 4.4 |
|
| 179 |
1.1 |
|
| 205 |
1.1 2.2 |
|
| 208 |
1.1 |