diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 79550af..73ce056 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -10,7 +10,7 @@ The Explain Error Plugin is a Jenkins plugin that provides AI-powered explanatio
- **GlobalConfigurationImpl**: Main plugin configuration class with `@Symbol("explainError")` for Configuration as Code support, handles migration from legacy enum-based configuration
- **BaseAIProvider**: Abstract base class for AI provider implementations with nested `Assistant` interface and `BaseProviderDescriptor` for extensibility
-- **OpenAIProvider** / **GeminiProvider** / **BedrockProvider** / **OllamaProvider**: LangChain4j-based AI service implementations with provider-specific configurations
+- **OpenAIProvider** / **GeminiProvider** / **AnthropicProvider** / **BedrockProvider** / **DeepSeekProvider** / **OllamaProvider**: LangChain4j-based AI service implementations with provider-specific configurations
- **ExplainErrorStep**: Pipeline step implementation for `explainError()` function
- **ConsoleExplainErrorAction**: Adds "Explain Error" button to console output for manual triggering
- **ConsoleExplainErrorActionFactory**: TransientActionFactory that dynamically injects ConsoleExplainErrorAction into all runs (new and existing)
@@ -39,6 +39,8 @@ src/main/java/io/jenkins/plugins/explain_error/
├── BaseAIProvider.java # Abstract AI service with Assistant interface
├── OpenAIProvider.java # OpenAI/LangChain4j implementation
├── GeminiProvider.java # Google Gemini/LangChain4j implementation
+ ├── AnthropicProvider.java # Anthropic Claude/LangChain4j implementation
+ ├── DeepSeekProvider.java # DeepSeek/LangChain4j implementation
├── BedrockProvider.java # AWS Bedrock/LangChain4j implementation
└── OllamaProvider.java # Ollama/LangChain4j implementation
```
diff --git a/README.md b/README.md
index c53f324..801e865 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,30 @@ unclassified:
enableExplanation: true
```
+**Anthropic Configuration:**
+```yaml
+unclassified:
+ explainError:
+ aiProvider:
+ anthropic:
+ apiKey: "${AI_API_KEY}"
+ model: "claude-3-5-sonnet-20241022"
+ # url: "" # Optional, leave empty for default
+ enableExplanation: true
+```
+
+**DeepSeek Configuration:**
+```yaml
+unclassified:
+ explainError:
+ aiProvider:
+ deepseek:
+ apiKey: "${AI_API_KEY}"
+ model: "deepseek-chat"
+ # url: "" # Optional, leave empty for default (https://api.deepseek.com)
+ enableExplanation: true
+```
+
**Ollama Configuration:**
```yaml
unclassified:
@@ -170,6 +194,18 @@ This allows you to manage the plugin configuration alongside your other Jenkins
- **Endpoint**: Leave empty for official Google AI API, or specify custom URL for Gemini-compatible services
- **Best for**: Fast, efficient analysis with competitive quality
+### Anthropic
+- **Models**: `claude-3-5-sonnet-20241022`, `claude-3-5-haiku-20241022`, `claude-3-opus-20240229`, etc.
+- **API Key**: Get from [Anthropic Console](https://console.anthropic.com/)
+- **Endpoint**: Leave empty for official Anthropic API, or specify custom URL for Claude-compatible services
+- **Best for**: Advanced reasoning, complex error analysis with Claude models
+
+### DeepSeek
+- **Models**: `deepseek-chat`, `deepseek-coder`, `deepseek-reasoner`
+- **API Key**: Get from [DeepSeek Platform](https://platform.deepseek.com/)
+- **Endpoint**: Leave empty for official DeepSeek API (`https://api.deepseek.com`), or specify custom URL
+- **Best for**: Cost-effective, coding-focused analysis
+
### AWS Bedrock
- **Models**: `anthropic.claude-3-5-sonnet-20240620-v1:0`, `eu.anthropic.claude-3-5-sonnet-20240620-v1:0` (EU cross-region), `meta.llama3-8b-instruct-v1:0`, `us.amazon.nova-lite-v1:0`, etc.
- **API Key**: Not required — uses AWS credential chain (instance profiles, environment variables, etc.)
diff --git a/pom.xml b/pom.xml
index cfb2599..7efb937 100644
--- a/pom.xml
+++ b/pom.xml
@@ -157,11 +157,19 @@
org.slf4j
- *
+ slf4j-api
com.fasterxml.jackson.core
- *
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
@@ -173,11 +181,19 @@
org.slf4j
- *
+ slf4j-api
com.fasterxml.jackson.core
- *
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
@@ -189,11 +205,19 @@
org.slf4j
- *
+ slf4j-api
com.fasterxml.jackson.core
- *
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
@@ -205,11 +229,43 @@
org.slf4j
- *
+ slf4j-api
com.fasterxml.jackson.core
- *
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+
+
+
+ dev.langchain4j
+ langchain4j-anthropic
+ ${langchain4j.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
diff --git a/src/main/java/io/jenkins/plugins/explain_error/provider/AnthropicProvider.java b/src/main/java/io/jenkins/plugins/explain_error/provider/AnthropicProvider.java
new file mode 100644
index 0000000..7b14676
--- /dev/null
+++ b/src/main/java/io/jenkins/plugins/explain_error/provider/AnthropicProvider.java
@@ -0,0 +1,121 @@
+package io.jenkins.plugins.explain_error.provider;
+
+import dev.langchain4j.model.anthropic.AnthropicChatModel;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.service.AiServices;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.Util;
+import hudson.model.AutoCompletionCandidates;
+import hudson.model.TaskListener;
+import hudson.util.FormValidation;
+import hudson.util.Secret;
+import io.jenkins.plugins.explain_error.ExplanationException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.model.Jenkins;
+import org.jenkinsci.Symbol;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.verb.GET;
+import org.kohsuke.stapler.verb.POST;
+
+public class AnthropicProvider extends BaseAIProvider {
+
+ private static final Logger LOGGER = Logger.getLogger(AnthropicProvider.class.getName());
+
+ protected Secret apiKey;
+
+ @DataBoundConstructor
+ public AnthropicProvider(String url, String model, Secret apiKey) {
+ super(url, model);
+ this.apiKey = apiKey;
+ }
+
+ public Secret getApiKey() {
+ return apiKey;
+ }
+
+ @Override
+ public Assistant createAssistant() {
+ ChatModel model = AnthropicChatModel.builder()
+ .baseUrl(Util.fixEmptyAndTrim(getUrl())) // Will use default if null
+ .apiKey(getApiKey().getPlainText())
+ .modelName(getModel())
+ .temperature(0.3)
+ .logRequests(LOGGER.isLoggable(Level.FINE))
+ .logResponses(LOGGER.isLoggable(Level.FINE))
+ .build();
+
+ return AiServices.create(Assistant.class, model);
+ }
+
+ @Override
+ public boolean isNotValid(@CheckForNull TaskListener listener) {
+ if (listener != null) {
+ if (Util.fixEmptyAndTrim(Secret.toString(getApiKey())) == null) {
+ listener.getLogger().println("No Api key configured for Anthropic.");
+ } else if (Util.fixEmptyAndTrim(getModel()) == null) {
+ listener.getLogger().println("No Model configured for Anthropic.");
+ }
+ }
+ return Util.fixEmptyAndTrim(Secret.toString(getApiKey())) == null ||
+ Util.fixEmptyAndTrim(getModel()) == null;
+ }
+
+ @Extension
+ @Symbol("anthropic")
+ public static class DescriptorImpl extends BaseProviderDescriptor {
+
+ private static final String[] MODELS = new String[]{
+ "claude-3-5-sonnet-20241022",
+ "claude-3-5-sonnet-20240620",
+ "claude-3-5-haiku-20241022",
+ "claude-3-opus-20240229",
+ "claude-3-sonnet-20240229",
+ "claude-3-haiku-20240307"
+ };
+
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return "Anthropic";
+ }
+
+ public String getDefaultModel() {
+ return "claude-3-5-sonnet-20241022";
+ }
+
+ @GET
+ @SuppressWarnings("lgtm[jenkins/no-permission-check]")
+ public AutoCompletionCandidates doAutoCompleteModel(@QueryParameter String value) {
+ AutoCompletionCandidates c = new AutoCompletionCandidates();
+ for (String model : MODELS) {
+ if (model.toLowerCase().startsWith(value.toLowerCase())) {
+ c.add(model);
+ }
+ }
+ return c;
+ }
+
+ /**
+ * Method to test the AI API configuration.
+ * This is called when the "Test Configuration" button is clicked.
+ */
+ @POST
+ public FormValidation doTestConfiguration(@QueryParameter("apiKey") Secret apiKey,
+ @QueryParameter("url") String url,
+ @QueryParameter("model") String model) throws ExplanationException {
+ Jenkins.get().checkPermission(Jenkins.ADMINISTER);
+
+ AnthropicProvider provider = new AnthropicProvider(url, model, apiKey);
+ try {
+ provider.explainError("Send 'Configuration test successful' to me.", null);
+ return FormValidation.ok("Configuration test successful! API connection is working properly.");
+ } catch (ExplanationException e) {
+ return FormValidation.error("Configuration test failed: " + e.getMessage(), e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/jenkins/plugins/explain_error/provider/DeepSeekProvider.java b/src/main/java/io/jenkins/plugins/explain_error/provider/DeepSeekProvider.java
new file mode 100644
index 0000000..6fa6894
--- /dev/null
+++ b/src/main/java/io/jenkins/plugins/explain_error/provider/DeepSeekProvider.java
@@ -0,0 +1,126 @@
+package io.jenkins.plugins.explain_error.provider;
+
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.request.ResponseFormat;
+import dev.langchain4j.model.openai.OpenAiChatModel;
+import dev.langchain4j.service.AiServices;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.Util;
+import hudson.model.AutoCompletionCandidates;
+import hudson.model.TaskListener;
+import hudson.util.FormValidation;
+import hudson.util.Secret;
+import io.jenkins.plugins.explain_error.ExplanationException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.model.Jenkins;
+import org.jenkinsci.Symbol;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.verb.GET;
+import org.kohsuke.stapler.verb.POST;
+
+public class DeepSeekProvider extends BaseAIProvider {
+
+ private static final Logger LOGGER = Logger.getLogger(DeepSeekProvider.class.getName());
+ private static final String DEFAULT_BASE_URL = "https://api.deepseek.com";
+
+ protected Secret apiKey;
+
+ @DataBoundConstructor
+ public DeepSeekProvider(String url, String model, Secret apiKey) {
+ super(url, model);
+ this.apiKey = apiKey;
+ }
+
+ public Secret getApiKey() {
+ return apiKey;
+ }
+
+ @Override
+ public Assistant createAssistant() {
+ String baseUrl = Util.fixEmptyAndTrim(getUrl());
+ if (baseUrl == null) {
+ baseUrl = DEFAULT_BASE_URL;
+ }
+
+ ChatModel model = OpenAiChatModel.builder()
+ .baseUrl(baseUrl)
+ .apiKey(getApiKey().getPlainText())
+ .modelName(getModel())
+ .temperature(0.3)
+ .responseFormat(ResponseFormat.JSON)
+ .logRequests(LOGGER.isLoggable(Level.FINE))
+ .logResponses(LOGGER.isLoggable(Level.FINE))
+ .build();
+
+ return AiServices.create(Assistant.class, model);
+ }
+
+ @Override
+ public boolean isNotValid(@CheckForNull TaskListener listener) {
+ if (listener != null) {
+ if (Util.fixEmptyAndTrim(Secret.toString(getApiKey())) == null) {
+ listener.getLogger().println("No Api key configured for DeepSeek.");
+ } else if (Util.fixEmptyAndTrim(getModel()) == null) {
+ listener.getLogger().println("No Model configured for DeepSeek.");
+ }
+ }
+ return Util.fixEmptyAndTrim(Secret.toString(getApiKey())) == null ||
+ Util.fixEmptyAndTrim(getModel()) == null;
+ }
+
+ @Extension
+ @Symbol("deepseek")
+ public static class DescriptorImpl extends BaseProviderDescriptor {
+
+ private static final String[] MODELS = new String[]{
+ "deepseek-chat",
+ "deepseek-coder",
+ "deepseek-reasoner"
+ };
+
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return "DeepSeek";
+ }
+
+ public String getDefaultModel() {
+ return "deepseek-chat";
+ }
+
+ @GET
+ @SuppressWarnings("lgtm[jenkins/no-permission-check]")
+ public AutoCompletionCandidates doAutoCompleteModel(@QueryParameter String value) {
+ AutoCompletionCandidates c = new AutoCompletionCandidates();
+ for (String model : MODELS) {
+ if (model.toLowerCase().startsWith(value.toLowerCase())) {
+ c.add(model);
+ }
+ }
+ return c;
+ }
+
+ /**
+ * Method to test the AI API configuration.
+ * This is called when the "Test Configuration" button is clicked.
+ */
+ @POST
+ public FormValidation doTestConfiguration(@QueryParameter("apiKey") Secret apiKey,
+ @QueryParameter("url") String url,
+ @QueryParameter("model") String model) throws ExplanationException {
+ Jenkins.get().checkPermission(Jenkins.ADMINISTER);
+
+ DeepSeekProvider provider = new DeepSeekProvider(url, model, apiKey);
+ try {
+ provider.explainError("Send 'Configuration test successful' to me.", null);
+ return FormValidation.ok("Configuration test successful! API connection is working properly.");
+ } catch (ExplanationException e) {
+ return FormValidation.error("Configuration test failed: " + e.getMessage(), e);
+ }
+ }
+ }
+}
diff --git a/src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/config.jelly b/src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/config.jelly
new file mode 100644
index 0000000..c59d400
--- /dev/null
+++ b/src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/config.jelly
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/help-model.html b/src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/help-model.html
new file mode 100644
index 0000000..ef2cb0e
--- /dev/null
+++ b/src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/help-model.html
@@ -0,0 +1,4 @@
+
+ A valid name for an Anthropic Claude model, e.g.
claude-3-5-sonnet-20241022,
claude-3-opus-20240229,
claude-3-haiku-20240307.
+ See
Anthropic Models for a list of available models.
+
diff --git a/src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/help-url.html b/src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/help-url.html
new file mode 100644
index 0000000..636c280
--- /dev/null
+++ b/src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/help-url.html
@@ -0,0 +1,4 @@
+
+
Leave this field empty for standard Anthropic API usage.
+
Specify a custom URL when using custom Anthropic proxies, enterprise endpoints or Anthropic compatible services.
+
diff --git a/src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/config.jelly b/src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/config.jelly
new file mode 100644
index 0000000..c59d400
--- /dev/null
+++ b/src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/config.jelly
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/help-model.html b/src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/help-model.html
new file mode 100644
index 0000000..c0e9fee
--- /dev/null
+++ b/src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/help-model.html
@@ -0,0 +1,4 @@
+
+ A valid name for a DeepSeek model, e.g.
deepseek-chat,
deepseek-coder.
+ See
DeepSeek Models for a list of available models.
+
diff --git a/src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/help-url.html b/src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/help-url.html
new file mode 100644
index 0000000..bb7c42a
--- /dev/null
+++ b/src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/help-url.html
@@ -0,0 +1,4 @@
+
+
Leave this field empty for standard DeepSeek API usage.
+
Specify a custom URL when using custom DeepSeek proxies, enterprise endpoints or DeepSeek compatible services.
+
diff --git a/src/test/java/io/jenkins/plugins/explain_error/CasCTest.java b/src/test/java/io/jenkins/plugins/explain_error/CasCTest.java
index 1198fe1..14d3ead 100644
--- a/src/test/java/io/jenkins/plugins/explain_error/CasCTest.java
+++ b/src/test/java/io/jenkins/plugins/explain_error/CasCTest.java
@@ -6,7 +6,9 @@
import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
import io.jenkins.plugins.casc.misc.junit.jupiter.WithJenkinsConfiguredWithCode;
+import io.jenkins.plugins.explain_error.provider.AnthropicProvider;
import io.jenkins.plugins.explain_error.provider.BaseAIProvider;
+import io.jenkins.plugins.explain_error.provider.DeepSeekProvider;
import io.jenkins.plugins.explain_error.provider.OllamaProvider;
import org.junit.jupiter.api.Test;
@@ -32,4 +34,24 @@ void loadNewConfig(JenkinsConfiguredWithCodeRule jcwcRule) {
assertEquals("gemma3:1b", provider.getModel());
assertEquals("http://localhost:11434", provider.getUrl());
}
+
+ @Test
+ @ConfiguredWithCode("casc_anthropic.yaml")
+ void loadAnthropicConfig(JenkinsConfiguredWithCodeRule jcwcRule) {
+ GlobalConfigurationImpl config = GlobalConfigurationImpl.get();
+ BaseAIProvider provider = config.getAiProvider();
+ assertInstanceOf(AnthropicProvider.class, provider);
+ assertEquals("claude-3-5-sonnet-20241022", provider.getModel());
+ assertEquals("https://api.anthropic.com", provider.getUrl());
+ }
+
+ @Test
+ @ConfiguredWithCode("casc_deepseek.yaml")
+ void loadDeepSeekConfig(JenkinsConfiguredWithCodeRule jcwcRule) {
+ GlobalConfigurationImpl config = GlobalConfigurationImpl.get();
+ BaseAIProvider provider = config.getAiProvider();
+ assertInstanceOf(DeepSeekProvider.class, provider);
+ assertEquals("deepseek-chat", provider.getModel());
+ assertEquals("https://api.deepseek.com", provider.getUrl());
+ }
}
diff --git a/src/test/java/io/jenkins/plugins/explain_error/provider/ProviderTest.java b/src/test/java/io/jenkins/plugins/explain_error/provider/ProviderTest.java
index cf27f5d..5d66b75 100644
--- a/src/test/java/io/jenkins/plugins/explain_error/provider/ProviderTest.java
+++ b/src/test/java/io/jenkins/plugins/explain_error/provider/ProviderTest.java
@@ -156,6 +156,14 @@ void testOllamaNullUrl() {
assertEquals("The provider is not properly configured.", result.getMessage());
}
+ @Test
+ void testAnthropicWithNullApiKey() {
+ BaseAIProvider provider = new AnthropicProvider(null, "test-model", null);
+ ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));
+
+ assertEquals("The provider is not properly configured.", result.getMessage());
+ }
+
@Test
void testBedrockNullModel() {
BaseAIProvider provider = new BedrockProvider(null, null, "eu-west-1");
@@ -164,6 +172,62 @@ void testBedrockNullModel() {
assertEquals("The provider is not properly configured.", result.getMessage());
}
+ @Test
+ void testAnthropicWithEmptyApiKey() {
+ BaseAIProvider provider = new AnthropicProvider(null, "test-model", Secret.fromString(""));
+ ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));
+
+ assertEquals("The provider is not properly configured.", result.getMessage());
+ }
+
+ @Test
+ void testAnthropicWithNullModel() {
+ BaseAIProvider provider = new AnthropicProvider(null, null, Secret.fromString("test-key"));
+ ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));
+
+ assertEquals("The provider is not properly configured.", result.getMessage());
+ }
+
+ @Test
+ void testAnthropicWithEmptyModel() {
+ BaseAIProvider provider = new AnthropicProvider(null, "", Secret.fromString("test-key"));
+ ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));
+
+ assertEquals("The provider is not properly configured.", result.getMessage());
+ }
+
+ @Test
+ void testDeepSeekWithNullApiKey() {
+ BaseAIProvider provider = new DeepSeekProvider(null, "test-model", null);
+ ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));
+
+ assertEquals("The provider is not properly configured.", result.getMessage());
+ }
+
+ @Test
+ void testDeepSeekWithEmptyApiKey() {
+ BaseAIProvider provider = new DeepSeekProvider(null, "test-model", Secret.fromString(""));
+ ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));
+
+ assertEquals("The provider is not properly configured.", result.getMessage());
+ }
+
+ @Test
+ void testDeepSeekWithNullModel() {
+ BaseAIProvider provider = new DeepSeekProvider(null, null, Secret.fromString("test-key"));
+ ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));
+
+ assertEquals("The provider is not properly configured.", result.getMessage());
+ }
+
+ @Test
+ void testDeepSeekWithEmptyModel() {
+ BaseAIProvider provider = new DeepSeekProvider(null, "", Secret.fromString("test-key"));
+ ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));
+
+ assertEquals("The provider is not properly configured.", result.getMessage());
+ }
+
@Test
void testBedrockEmptyModel() {
BaseAIProvider provider = new BedrockProvider(null, "", "eu-west-1");
diff --git a/src/test/java/io/jenkins/plugins/explain_error/provider/TestAnthropicProvider.java b/src/test/java/io/jenkins/plugins/explain_error/provider/TestAnthropicProvider.java
new file mode 100644
index 0000000..a82d101
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/explain_error/provider/TestAnthropicProvider.java
@@ -0,0 +1,102 @@
+package io.jenkins.plugins.explain_error.provider;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.util.Secret;
+import io.jenkins.plugins.explain_error.JenkinsLogAnalysis;
+
+import org.jenkinsci.Symbol;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+public class TestAnthropicProvider extends AnthropicProvider {
+
+ private boolean throwError = false;
+ private JenkinsLogAnalysis answerMessage = new JenkinsLogAnalysis(
+ "Request was successful", null, null, null);
+ private int callCount = 0;
+ private String providerName = "TestAnthropic";
+
+ // Captured parameters from last analyzeLogs call
+ private String lastErrorLogs;
+ private String lastLanguage;
+ private String lastCustomContext;
+
+ @DataBoundConstructor
+ public TestAnthropicProvider() {
+ super("https://localhost:1234", "test-anthropic-model", Secret.fromString("test-api-key"));
+ }
+
+ @Override
+ public Assistant createAssistant() {
+ return new Assistant() {
+ @Override
+ public JenkinsLogAnalysis analyzeLogs(String errorLogs, String language, String customContext) {
+ if (throwError) {
+ throw new RuntimeException("Request failed.");
+ }
+ // Capture parameters for test verification
+ lastErrorLogs = errorLogs;
+ lastLanguage = language;
+ lastCustomContext = customContext;
+ callCount++;
+ return answerMessage;
+ }
+ };
+ }
+
+ public void setThrowError(boolean throwError) {
+ this.throwError = throwError;
+ }
+
+ public void setApiKey(Secret apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ public void setAnswerMessage(String answerMessage) {
+ this.answerMessage = new JenkinsLogAnalysis(answerMessage, null, null, null);
+ }
+
+ public int getCallCount() {
+ return callCount;
+ }
+
+ public String getLastErrorLogs() {
+ return lastErrorLogs;
+ }
+
+ public String getLastLanguage() {
+ return lastLanguage;
+ }
+
+ public String getLastCustomContext() {
+ return lastCustomContext;
+ }
+
+ public void setProviderName(String providerName) {
+ this.providerName = providerName;
+ }
+
+ @Override
+ public String getProviderName() {
+ return providerName;
+ }
+
+ @Extension
+ @Symbol("testAnthropic")
+ public static class DescriptorImpl extends BaseProviderDescriptor {
+
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return "TestAnthropic";
+ }
+
+ public String getDefaultModel() {
+ return "test-anthropic-model";
+ }
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/explain_error/provider/TestDeepSeekProvider.java b/src/test/java/io/jenkins/plugins/explain_error/provider/TestDeepSeekProvider.java
new file mode 100644
index 0000000..e4fdf36
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/explain_error/provider/TestDeepSeekProvider.java
@@ -0,0 +1,102 @@
+package io.jenkins.plugins.explain_error.provider;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import hudson.Extension;
+import hudson.util.Secret;
+import io.jenkins.plugins.explain_error.JenkinsLogAnalysis;
+
+import org.jenkinsci.Symbol;
+import org.kohsuke.stapler.DataBoundConstructor;
+
+public class TestDeepSeekProvider extends DeepSeekProvider {
+
+ private boolean throwError = false;
+ private JenkinsLogAnalysis answerMessage = new JenkinsLogAnalysis(
+ "Request was successful", null, null, null);
+ private int callCount = 0;
+ private String providerName = "TestDeepSeek";
+
+ // Captured parameters from last analyzeLogs call
+ private String lastErrorLogs;
+ private String lastLanguage;
+ private String lastCustomContext;
+
+ @DataBoundConstructor
+ public TestDeepSeekProvider() {
+ super("https://localhost:1234", "test-deepseek-model", Secret.fromString("test-api-key"));
+ }
+
+ @Override
+ public Assistant createAssistant() {
+ return new Assistant() {
+ @Override
+ public JenkinsLogAnalysis analyzeLogs(String errorLogs, String language, String customContext) {
+ if (throwError) {
+ throw new RuntimeException("Request failed.");
+ }
+ // Capture parameters for test verification
+ lastErrorLogs = errorLogs;
+ lastLanguage = language;
+ lastCustomContext = customContext;
+ callCount++;
+ return answerMessage;
+ }
+ };
+ }
+
+ public void setThrowError(boolean throwError) {
+ this.throwError = throwError;
+ }
+
+ public void setApiKey(Secret apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ public void setAnswerMessage(String answerMessage) {
+ this.answerMessage = new JenkinsLogAnalysis(answerMessage, null, null, null);
+ }
+
+ public int getCallCount() {
+ return callCount;
+ }
+
+ public String getLastErrorLogs() {
+ return lastErrorLogs;
+ }
+
+ public String getLastLanguage() {
+ return lastLanguage;
+ }
+
+ public String getLastCustomContext() {
+ return lastCustomContext;
+ }
+
+ public void setProviderName(String providerName) {
+ this.providerName = providerName;
+ }
+
+ @Override
+ public String getProviderName() {
+ return providerName;
+ }
+
+ @Extension
+ @Symbol("testDeepSeek")
+ public static class DescriptorImpl extends BaseProviderDescriptor {
+
+ @NonNull
+ @Override
+ public String getDisplayName() {
+ return "TestDeepSeek";
+ }
+
+ public String getDefaultModel() {
+ return "test-deepseek-model";
+ }
+ }
+}
diff --git a/src/test/resources/io/jenkins/plugins/explain_error/casc_anthropic.yaml b/src/test/resources/io/jenkins/plugins/explain_error/casc_anthropic.yaml
new file mode 100644
index 0000000..c04fc90
--- /dev/null
+++ b/src/test/resources/io/jenkins/plugins/explain_error/casc_anthropic.yaml
@@ -0,0 +1,8 @@
+unclassified:
+ explainError:
+ aiProvider:
+ anthropic:
+ apiKey: "test-anthropic-api-key"
+ model: "claude-3-5-sonnet-20241022"
+ url: "https://api.anthropic.com"
+ enableExplanation: true
diff --git a/src/test/resources/io/jenkins/plugins/explain_error/casc_deepseek.yaml b/src/test/resources/io/jenkins/plugins/explain_error/casc_deepseek.yaml
new file mode 100644
index 0000000..3e7c792
--- /dev/null
+++ b/src/test/resources/io/jenkins/plugins/explain_error/casc_deepseek.yaml
@@ -0,0 +1,8 @@
+unclassified:
+ explainError:
+ aiProvider:
+ deepseek:
+ apiKey: "test-deepseek-api-key"
+ model: "deepseek-chat"
+ url: "https://api.deepseek.com"
+ enableExplanation: true