AiOptions.java
package org.egothor.methodatlas.ai;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Objects;
/**
* Immutable configuration describing how AI-based enrichment should be
* performed during a {@code MethodAtlasApp} execution.
*
* <p>
* This record aggregates all runtime parameters required by the AI integration
* layer, including provider selection, model identification, authentication
* configuration, taxonomy selection, request limits, and retry behavior.
* </p>
*
* <p>
* Instances of this record are typically constructed using the associated
* {@link Builder} and passed to the AI subsystem when initializing an
* {@link AiSuggestionEngine}. The configuration is immutable once constructed
* and therefore safe to share between concurrent components.
* </p>
*
* <h2>Configuration Responsibilities</h2>
*
* <ul>
* <li>AI provider selection and endpoint configuration</li>
* <li>model name resolution</li>
* <li>API key discovery</li>
* <li>taxonomy configuration for security classification</li>
* <li>input size limits for class source submission</li>
* <li>network timeout configuration</li>
* <li>retry policy for transient AI failures</li>
* </ul>
*
* <p>
* Default values are supplied by the {@link Builder} when parameters are not
* explicitly provided.
* </p>
*
* @param enabled whether AI enrichment is enabled
* @param provider AI provider used to perform analysis
* @param modelName provider-specific model identifier
* @param baseUrl base API endpoint used by the selected provider
* @param apiKey API key used for authentication, if provided directly
* @param apiKeyEnv environment variable name containing the API key
* @param taxonomyFile optional path to an external taxonomy definition
* @param taxonomyMode built-in taxonomy mode to use when no file is provided
* @param maxClassChars maximum number of characters allowed for class source
* submitted to the AI provider
* @param timeout request timeout applied to AI calls
* @param maxRetries number of retry attempts for failed AI operations
* @param confidence whether the AI provider should be asked to include a
* confidence score for each security-relevant method
* classification; requires model support and increases
* token usage slightly
* @param redactComments whether comments are replaced by blanks in the class
* source before it is sent to the AI provider (data
* minimisation); fail-closed when no redactor supports a
* scanned language
* @param apiVersion Azure OpenAI REST API version used to construct the
* deployment endpoint; only consulted when
* {@link AiProvider#AZURE_OPENAI} is selected; defaults
* to {@link #DEFAULT_API_VERSION}
* @param promptTemplates the effective prompt templates (built-in defaults with any
* operator overrides already applied) used both to render the
* prompts sent to the provider and to compute the per-template
* hashes recorded in the reproducibility receipt; never
* {@code null}
*
* @see AiSuggestionEngine
* @see Builder
*/
public record AiOptions(boolean enabled, AiProvider provider, String modelName, String baseUrl, String apiKey,
String apiKeyEnv, Path taxonomyFile, TaxonomyMode taxonomyMode, int maxClassChars, Duration timeout,
int maxRetries, boolean confidence, boolean redactComments, String apiVersion, PromptTemplateSet promptTemplates) {
/**
* Built-in taxonomy modes used for security classification.
*
* <p>
* These modes determine which internal taxonomy definition is supplied to the
* AI provider when an external taxonomy file is not configured.
* </p>
*
* <ul>
* <li>{@link #DEFAULT} – general-purpose taxonomy suitable for human
* readability</li>
* <li>{@link #OPTIMIZED} – compact taxonomy optimized for AI classification
* accuracy</li>
* </ul>
*/
public enum TaxonomyMode {
/**
* Standard taxonomy definition emphasizing clarity and documentation.
*/
DEFAULT,
/**
* Reduced taxonomy optimized for improved AI classification reliability.
*/
OPTIMIZED
}
/**
* Default model identifier used when no model is explicitly configured.
*
* <p>
* This constant is intentionally public so that governance processes can
* locate and track the approved fallback model in version control without
* searching through builder internals.
* </p>
*/
public static final String DEFAULT_MODEL = "qwen2.5-coder:7b";
/**
* Default Azure OpenAI REST API version used when {@link AiProvider#AZURE_OPENAI}
* is selected and no explicit version is configured.
*
* <p>
* This value targets the generally-available Chat Completions API. Newer
* preview versions may be required to access features such as structured
* outputs or reasoning models; override via {@link Builder#apiVersion(String)}
* or the {@code apiVersion} YAML field when needed.
* </p>
*/
public static final String DEFAULT_API_VERSION = "2024-02-01";
/**
* Canonical constructor performing validation of configuration parameters.
*
* <p>
* The constructor enforces basic invariants required for correct operation of
* the AI integration layer. Invalid values result in an
* {@link IllegalArgumentException}.
* </p>
*
* @throws NullPointerException if required parameters such as
* {@code provider}, {@code modelName},
* {@code timeout}, or {@code taxonomyMode} are
* {@code null}
* @throws IllegalArgumentException if configuration values violate required
* constraints
*/
public AiOptions {
Objects.requireNonNull(provider, "provider");
Objects.requireNonNull(modelName, "modelName");
Objects.requireNonNull(timeout, "timeout");
Objects.requireNonNull(taxonomyMode, "taxonomyMode");
Objects.requireNonNull(promptTemplates, "promptTemplates");
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("baseUrl must not be blank");
}
if (maxClassChars <= 0) {
throw new IllegalArgumentException("maxClassChars must be > 0");
}
if (maxRetries < 0) {
throw new IllegalArgumentException("maxRetries must be >= 0");
}
if (apiVersion == null || apiVersion.isBlank()) {
throw new IllegalArgumentException("apiVersion must not be blank");
}
}
/**
* Creates a new {@link Builder} used to construct {@link AiOptions} instances.
*
* <p>
* The builder supplies sensible defaults for most configuration values and
* allows incremental customization before producing the final immutable
* configuration record.
* </p>
*
* @return new builder instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Resolves the effective API key used for authenticating AI provider requests.
*
* <p>
* The resolution strategy is:
* </p>
*
* <ol>
* <li>If {@link #apiKey()} is defined and non-empty, it is returned.</li>
* <li>If {@link #apiKeyEnv()} is defined, the corresponding environment
* variable is resolved using {@link System#getenv(String)}.</li>
* <li>If neither source yields a value, {@code null} is returned.</li>
* </ol>
*
* @return resolved API key or {@code null} if none is available
*/
public String resolvedApiKey() {
if (apiKey != null && !apiKey.isBlank()) {
return apiKey;
}
if (apiKeyEnv != null && !apiKeyEnv.isBlank()) {
String value = System.getenv(apiKeyEnv);
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
/**
* Mutable builder used to construct validated {@link AiOptions} instances.
*
* <p>
* The builder follows the conventional staged construction pattern, allowing
* optional parameters to be supplied before producing the final immutable
* configuration record via {@link #build()}.
* </p>
*
* <p>
* Reasonable defaults are provided for most parameters so that only
* provider-specific details typically need to be configured explicitly.
* </p>
*/
public static final class Builder {
private boolean enabled;
private AiProvider provider = AiProvider.AUTO;
private String modelName = DEFAULT_MODEL;
private String baseUrl;
private String apiKey;
private String apiKeyEnv;
private Path taxonomyFile;
private TaxonomyMode taxonomyMode = TaxonomyMode.DEFAULT;
private int maxClassChars = 40_000;
private Duration timeout = Duration.ofSeconds(90);
private int maxRetries = 1;
private boolean confidence;
private boolean redactComments;
private String apiVersion = DEFAULT_API_VERSION;
private PromptTemplateSet promptTemplates = PromptTemplateSet.defaults();
/**
* Enables or disables AI enrichment.
*
* @param enabled {@code true} to enable AI integration
* @return this builder
*/
public Builder enabled(boolean enabled) {
this.enabled = enabled;
return this;
}
/**
* Selects the AI provider.
*
* @param provider provider implementation to use
* @return this builder
*/
public Builder provider(AiProvider provider) {
this.provider = provider;
return this;
}
/**
* Specifies the provider-specific model identifier.
*
* @param modelName name of the model to use
* @return this builder
*/
public Builder modelName(String modelName) {
this.modelName = modelName;
return this;
}
/**
* Sets the base API endpoint used by the provider.
*
* @param baseUrl base URL of the provider API
* @return this builder
*/
public Builder baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
/**
* Sets the API key used for authentication.
*
* @param apiKey API key value
* @return this builder
*/
public Builder apiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
/**
* Specifies the environment variable that stores the API key.
*
* @param apiKeyEnv environment variable name
* @return this builder
*/
public Builder apiKeyEnv(String apiKeyEnv) {
this.apiKeyEnv = apiKeyEnv;
return this;
}
/**
* Specifies an external taxonomy definition file.
*
* @param taxonomyFile path to taxonomy definition
* @return this builder
*/
public Builder taxonomyFile(Path taxonomyFile) {
this.taxonomyFile = taxonomyFile;
return this;
}
/**
* Selects the built-in taxonomy mode.
*
* @param taxonomyMode taxonomy variant
* @return this builder
*/
public Builder taxonomyMode(TaxonomyMode taxonomyMode) {
this.taxonomyMode = taxonomyMode;
return this;
}
/**
* Sets the maximum size of class source submitted to the AI provider.
*
* @param maxClassChars maximum allowed character count
* @return this builder
*/
public Builder maxClassChars(int maxClassChars) {
this.maxClassChars = maxClassChars;
return this;
}
/**
* Sets the timeout applied to AI requests.
*
* @param timeout request timeout
* @return this builder
*/
public Builder timeout(Duration timeout) {
this.timeout = timeout;
return this;
}
/**
* Sets the retry limit for AI requests.
*
* @param maxRetries retry count
* @return this builder
*/
public Builder maxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}
/**
* Enables or disables AI confidence scoring.
*
* <p>
* When enabled, the prompt instructs the AI provider to return a
* {@code confidence} value for each method classification. The value is
* included in the output as an {@code ai_confidence} column.
* </p>
*
* @param confidence {@code true} to request confidence scores
* @return this builder
*/
public Builder confidence(boolean confidence) {
this.confidence = confidence;
return this;
}
/**
* Enables or disables comment redaction of the class source sent to the AI
* provider.
*
* <p>
* When enabled, every comment is blanked before the source leaves the
* process and the run fails closed if a scanned language has no redactor.
* The default is {@code false}.
* </p>
*
* @param redactComments {@code true} to redact comments
* @return this builder
*/
public Builder redactComments(boolean redactComments) {
this.redactComments = redactComments;
return this;
}
/**
* Sets the Azure OpenAI REST API version used to construct the deployment
* endpoint.
*
* <p>
* This value is only used when {@link AiProvider#AZURE_OPENAI} is selected.
* It is appended as the {@code api-version} query parameter to the chat
* completions URL. The default value is {@link AiOptions#DEFAULT_API_VERSION}.
* </p>
*
* @param apiVersion REST API version string, for example {@code 2024-02-01}
* @return this builder
*/
public Builder apiVersion(String apiVersion) {
this.apiVersion = apiVersion;
return this;
}
/**
* Sets the effective prompt templates.
*
* <p>
* Callers pass the resolved set — the built-in defaults with any operator
* overrides already applied and validated. When not set, the built-in
* {@link PromptTemplateSet#defaults()} are used.
* </p>
*
* @param promptTemplates the resolved template set; must not be {@code null}
* @return this builder
* @throws NullPointerException if {@code promptTemplates} is {@code null}
*/
public Builder promptTemplates(PromptTemplateSet promptTemplates) {
this.promptTemplates = Objects.requireNonNull(promptTemplates, "promptTemplates");
return this;
}
/**
* Builds the final immutable {@link AiOptions} configuration.
*
* <p>
* If no base URL is explicitly supplied, a provider-specific default endpoint
* is selected automatically.
* </p>
*
* @return validated AI configuration
*/
public AiOptions build() {
AiProvider effectiveProvider = provider == null ? AiProvider.AUTO : provider;
String effectiveBaseUrl = baseUrl;
if (effectiveBaseUrl == null || effectiveBaseUrl.isBlank()) {
effectiveBaseUrl = switch (effectiveProvider) {
case AUTO, OLLAMA -> "http://localhost:11434";
case OPENAI -> "https://api.openai.com";
case OPENROUTER -> "https://openrouter.ai/api";
case ANTHROPIC -> "https://api.anthropic.com";
case GROQ -> "https://api.groq.com/openai";
case XAI -> "https://api.x.ai/v1";
case GITHUB_MODELS -> "https://models.inference.ai.azure.com";
case MISTRAL -> "https://api.mistral.ai/v1";
case AZURE_OPENAI -> throw new IllegalArgumentException(
"baseUrl is required for AZURE_OPENAI: set it to https://<resource>.openai.azure.com");
};
}
return new AiOptions(enabled, effectiveProvider, modelName, effectiveBaseUrl, apiKey, apiKeyEnv,
taxonomyFile, taxonomyMode, maxClassChars, timeout, maxRetries, confidence, redactComments, apiVersion,
promptTemplates);
}
}
}