Skip to content

Add support for Anthropic, Azure OpenAI, and DeepSeek providers#78

Open
shenxianpeng wants to merge 11 commits intomainfrom
feature/more-providers
Open

Add support for Anthropic, Azure OpenAI, and DeepSeek providers#78
shenxianpeng wants to merge 11 commits intomainfrom
feature/more-providers

Conversation

@shenxianpeng
Copy link
Member

@shenxianpeng shenxianpeng commented Jan 31, 2026

Testing done

Submitter checklist

  • Make sure you are opening from a topic/feature/bugfix branch (right side) and not your main branch!
  • Ensure that the pull request title represents the desired changelog entry
  • Please describe what you did
  • Link to relevant issues in GitHub or Jira
  • Link to relevant pull requests, esp. upstream and downstream changes
  • Ensure you have provided tests that demonstrate the feature works or the issue is fixed

@shenxianpeng shenxianpeng added the enhancement For changelog: Minor enhancement. use `major-rfe` for changes to be highlighted label Jan 31, 2026
@shenxianpeng shenxianpeng requested a review from Copilot January 31, 2026 15:26
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds support for three new AI providers to the Explain Error Plugin: Anthropic (Claude), Azure OpenAI, and DeepSeek. These additions follow the established provider pattern using LangChain4j integration and extend the plugin's compatibility with popular enterprise and cost-effective AI services.

Changes:

  • Adds three new AI provider implementations (AnthropicProvider, AzureOpenAIProvider, DeepSeekProvider) with complete validation, configuration, and LangChain4j integration
  • Adds comprehensive unit tests for all three providers covering validation scenarios and provider creation
  • Adds Configuration as Code (CasC) tests for the new providers
  • Updates README documentation with detailed provider descriptions, configuration examples, and usage guidance
  • Adds UI configuration files (Jelly) with helpful descriptions and autocomplete support
  • Updates pom.xml with new LangChain4j dependencies for Anthropic and Azure OpenAI, plus Netty BOM for version consistency

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/main/java/io/jenkins/plugins/explain_error/provider/AnthropicProvider.java Implements Anthropic Claude provider with API key authentication, model validation, and LangChain4j integration
src/main/java/io/jenkins/plugins/explain_error/provider/AzureOpenAIProvider.java Implements Azure OpenAI provider requiring endpoint URL, deployment name, and API key
src/main/java/io/jenkins/plugins/explain_error/provider/DeepSeekProvider.java Implements DeepSeek provider using OpenAI-compatible API with optional custom endpoint support
src/test/java/io/jenkins/plugins/explain_error/provider/ProviderTest.java Adds comprehensive unit tests for all three providers covering null/empty validation scenarios
src/test/java/io/jenkins/plugins/explain_error/CasCTest.java Adds CasC integration tests for all three providers to verify YAML configuration loading
src/main/resources/io/jenkins/plugins/explain_error/provider/AnthropicProvider/config.jelly UI configuration with API key, optional URL, and model fields with autocomplete
src/main/resources/io/jenkins/plugins/explain_error/provider/AzureOpenAIProvider/config.jelly UI configuration with required endpoint, deployment name, and API key fields
src/main/resources/io/jenkins/plugins/explain_error/provider/DeepSeekProvider/config.jelly UI configuration with API key, optional URL, and model fields with autocomplete
pom.xml Adds langchain4j-anthropic and langchain4j-azure-open-ai dependencies with proper exclusions, plus Netty BOM for version management
README.md Updates provider list, adds detailed sections for each new provider with configuration examples and usage guidance

Comment on lines +158 to +310

// ============= Anthropic Provider Tests =============

@Test
void testAnthropicWithNullApiKey() {
BaseAIProvider provider = new AnthropicProvider(null, "claude-3-5-sonnet-20241022", null);
ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));

assertEquals("The provider is not properly configured.", result.getMessage());
}

@Test
void testAnthropicWithEmptyApiKey() {
BaseAIProvider provider = new AnthropicProvider(null, "claude-3-5-sonnet-20241022", 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 testAnthropicProviderCreation() {
BaseAIProvider provider = new AnthropicProvider(null, "claude-3-5-sonnet-20241022", Secret.fromString("test-key"));
assertEquals("claude-3-5-sonnet-20241022", provider.getModel());
assertEquals(null, provider.getUrl());
}

@Test
void testAnthropicProviderWithCustomUrl() {
BaseAIProvider provider = new AnthropicProvider("https://custom-anthropic.example.com", "claude-3-5-sonnet-20241022", Secret.fromString("test-key"));
assertEquals("claude-3-5-sonnet-20241022", provider.getModel());
assertEquals("https://custom-anthropic.example.com", provider.getUrl());
}

// ============= Azure OpenAI Provider Tests =============

@Test
void testAzureOpenAIWithNullApiKey() {
BaseAIProvider provider = new AzureOpenAIProvider("https://test.openai.azure.com", "gpt-4.1", null);
ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));

assertEquals("The provider is not properly configured.", result.getMessage());
}

@Test
void testAzureOpenAIWithEmptyApiKey() {
BaseAIProvider provider = new AzureOpenAIProvider("https://test.openai.azure.com", "gpt-4.1", Secret.fromString(""));
ExplanationException result = assertThrows(ExplanationException.class, () -> provider.explainError("Test error", null));

assertEquals("The provider is not properly configured.", result.getMessage());
}

@Test
void testAzureOpenAIWithNullEndpoint() {
BaseAIProvider provider = new AzureOpenAIProvider(null, "gpt-4.1", 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 testAzureOpenAIWithEmptyEndpoint() {
BaseAIProvider provider = new AzureOpenAIProvider("", "gpt-4.1", 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 testAzureOpenAIWithNullDeployment() {
BaseAIProvider provider = new AzureOpenAIProvider("https://test.openai.azure.com", 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 testAzureOpenAIWithEmptyDeployment() {
BaseAIProvider provider = new AzureOpenAIProvider("https://test.openai.azure.com", "", 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 testAzureOpenAIProviderCreation() {
BaseAIProvider provider = new AzureOpenAIProvider("https://test.openai.azure.com", "my-deployment", Secret.fromString("test-key"));
assertEquals("my-deployment", provider.getModel());
assertEquals("https://test.openai.azure.com", provider.getUrl());
}

// ============= DeepSeek Provider Tests =============

@Test
void testDeepSeekWithNullApiKey() {
BaseAIProvider provider = new DeepSeekProvider(null, "deepseek-chat", 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, "deepseek-chat", 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 testDeepSeekProviderCreation() {
BaseAIProvider provider = new DeepSeekProvider(null, "deepseek-chat", Secret.fromString("test-key"));
assertEquals("deepseek-chat", provider.getModel());
assertEquals(null, provider.getUrl());
}

@Test
void testDeepSeekProviderWithCustomUrl() {
BaseAIProvider provider = new DeepSeekProvider("https://custom-deepseek.example.com", "deepseek-coder", Secret.fromString("test-key"));
assertEquals("deepseek-coder", provider.getModel());
assertEquals("https://custom-deepseek.example.com", provider.getUrl());
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file uses AnthropicProvider, AzureOpenAIProvider, and DeepSeekProvider classes but does not import them. Add the following imports at the top of the file:

  • import io.jenkins.plugins.explain_error.provider.AnthropicProvider;
  • import io.jenkins.plugins.explain_error.provider.AzureOpenAIProvider;
  • import io.jenkins.plugins.explain_error.provider.DeepSeekProvider;

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +78
@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());

AnthropicProvider anthropicProvider = (AnthropicProvider) provider;
assertNotNull(anthropicProvider.getApiKey());
assertEquals("test-anthropic-key", anthropicProvider.getApiKey().getPlainText());
}

@Test
@ConfiguredWithCode("casc_azure.yaml")
void loadAzureConfig(JenkinsConfiguredWithCodeRule jcwcRule) {
GlobalConfigurationImpl config = GlobalConfigurationImpl.get();
BaseAIProvider provider = config.getAiProvider();
assertInstanceOf(AzureOpenAIProvider.class, provider);
assertEquals("gpt-4.1-deployment", provider.getModel());
assertEquals("https://test-resource.openai.azure.com", provider.getUrl());

AzureOpenAIProvider azureProvider = (AzureOpenAIProvider) provider;
assertNotNull(azureProvider.getApiKey());
assertEquals("test-azure-key", azureProvider.getApiKey().getPlainText());
}

@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());

DeepSeekProvider deepseekProvider = (DeepSeekProvider) provider;
assertNotNull(deepseekProvider.getApiKey());
assertEquals("test-deepseek-key", deepseekProvider.getApiKey().getPlainText());
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test references CasC YAML files that are not included in this PR. The files casc_anthropic.yaml, casc_azure.yaml, and casc_deepseek.yaml must be created in src/test/resources/io/jenkins/plugins/explain_error/ for these tests to pass. These files should follow the pattern of existing CasC test files like casc_new.yaml.

Copilot uses AI. Check for mistakes.
Comment on lines 96 to 102
if (model.toLowerCase().contains(value.toLowerCase())) {
c.add(model);
}
}
return c;
}

Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The autocompletion logic uses contains() instead of startsWith() which is inconsistent with OpenAIProvider (line 101 in OpenAIProvider.java). While contains() is more flexible, it creates an inconsistent user experience across providers. For consistency, consider using startsWith() like OpenAIProvider, or update OpenAIProvider to use contains() for all providers.

Copilot uses AI. Check for mistakes.
Comment on lines 96 to 103
public AutoCompletionCandidates doAutoCompleteModel(@QueryParameter String value) {
AutoCompletionCandidates c = new AutoCompletionCandidates();
for (String model : COMMON_MODELS) {
if (model.toLowerCase().contains(value.toLowerCase())) {
c.add(model);
}
}
return c;
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The autocompletion logic uses contains() instead of startsWith() which is inconsistent with OpenAIProvider (line 101 in OpenAIProvider.java). While contains() is more flexible, it creates an inconsistent user experience across providers. For consistency, consider using startsWith() like OpenAIProvider, or update OpenAIProvider to use contains() for all providers.

Copilot uses AI. Check for mistakes.
Comment on lines 99 to 106
public AutoCompletionCandidates doAutoCompleteModel(@QueryParameter String value) {
AutoCompletionCandidates c = new AutoCompletionCandidates();
for (String model : MODELS) {
if (model.toLowerCase().contains(value.toLowerCase())) {
c.add(model);
}
}
return c;
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The autocompletion logic uses contains() instead of startsWith() which is inconsistent with OpenAIProvider (line 101 in OpenAIProvider.java). While contains() is more flexible, it creates an inconsistent user experience across providers. For consistency, consider using startsWith() like OpenAIProvider, or update OpenAIProvider to use contains() for all providers.

Copilot uses AI. Check for mistakes.
shenxianpeng and others added 5 commits January 31, 2026 17:36
…picProvider.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…penAIProvider.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…picProvider.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…penAIProvider.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ekProvider.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@shenxianpeng shenxianpeng marked this pull request as ready for review January 31, 2026 15:37
@shenxianpeng shenxianpeng requested a review from a team as a code owner January 31, 2026 15:37
Copy link
Contributor

@panicking panicking left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shenxianpeng please try to improve the commit message, and reduce code to avoid copy and paste

- Fix error message in AzureOpenAIProvider (line 62) to say 'No Endpoint' instead of 'No API key'
- Remove else-if logic in AnthropicProvider validation to show all missing config messages
- Standardize autocompletion across all providers to use startsWith() instead of contains()
- Rename COMMON_MODELS to COMMON_DEPLOYMENT_NAMES in AzureOpenAIProvider with clarifying comments
- Add <optional>true</optional> to langchain4j-anthropic and langchain4j-azure-open-ai dependencies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement For changelog: Minor enhancement. use `major-rfe` for changes to be highlighted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants