Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`axonflow.listLLMProviders()`** + `listLLMProviders(String type, Boolean enabled)` — list configured LLM providers and their per-provider health snapshot. Calls `GET /api/v1/llm-providers`. New `LLMProvider` and `LLMProviderHealth` types in `com.getaxonflow.sdk.types`. Async variant `listLLMProvidersAsync()`. Closes the parity gap with the Python SDK's `list_providers()` and the Go SDK's `ListProviders()`.

## [6.1.0] - 2026-04-25 — Plugin Batch 1 explainability fields on MCP responses

Minor release. Surfaces fields the AxonFlow agent has emitted since v7.1.0 (Plugin Batch 1) but the SDK didn't declare. Pure field-additions on existing methods — additive only, no breaking changes. The pre-existing constructors are preserved as source-compat overloads. Documented in OpenAPI via platform v7.4.3.
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/com/getaxonflow/sdk/AxonFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -1743,6 +1743,59 @@ public CompletableFuture<RollbackPlanResponse> rollbackPlanAsync(
// MCP Connectors
// ========================================================================

/**
* Lists configured LLM providers and their per-provider health snapshot.
*
* <p>Calls {@code GET /api/v1/llm-providers}. Mirrors the Python SDK's {@code
* list_providers()}, the Go SDK's {@code ListProviders()}, and the TypeScript
* SDK's {@code listProviders()}.
*
* @return list of configured providers
*/
public List<LLMProvider> listLLMProviders() {
return listLLMProviders(null, null);
}

/**
* Lists configured LLM providers, optionally filtered by type and/or enabled flag.
*
* @param type filter by provider type (e.g. {@code "openai"}, {@code "anthropic"}); null for no
* filter
* @param enabled filter by the enabled boolean; null for no filter
* @return list of matching providers
*/
public List<LLMProvider> listLLMProviders(String type, Boolean enabled) {
return retryExecutor.execute(
() -> {
StringBuilder path = new StringBuilder("/api/v1/llm-providers");
boolean hasQuery = false;
if (type != null && !type.isEmpty()) {
path.append('?').append("type=").append(type);
hasQuery = true;
}
if (enabled != null) {
path.append(hasQuery ? '&' : '?').append("enabled=").append(enabled);
}
Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null);
try (Response response = httpClient.newCall(httpRequest).execute()) {
JsonNode node = parseResponseNode(response);
JsonNode providers = node.has("providers") ? node.get("providers") : node;
return objectMapper.convertValue(
providers, new TypeReference<List<LLMProvider>>() {});
}
},
"listLLMProviders");
}

/**
* Asynchronously lists configured LLM providers.
*
* @return a future containing the list of providers
*/
public CompletableFuture<List<LLMProvider>> listLLMProvidersAsync() {
return CompletableFuture.supplyAsync(this::listLLMProviders, asyncExecutor);
}

/**
* Lists available MCP connectors.
*
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/com/getaxonflow/sdk/types/LLMProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2025 AxonFlow
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getaxonflow.sdk.types;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* A registered LLM provider returned by {@code GET /api/v1/llm-providers}.
*
* <p>Mirrors {@code LLMProvider} in the Python and Go SDKs and the {@code LLMProvider}
* TypeScript interface.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class LLMProvider {

private final String name;
private final String type;
private final boolean enabled;
private final int priority;
private final int weight;
private final boolean hasApiKey;
private final LLMProviderHealth health;

public LLMProvider(
@JsonProperty("name") String name,
@JsonProperty("type") String type,
@JsonProperty("enabled") boolean enabled,
@JsonProperty("priority") int priority,
@JsonProperty("weight") int weight,
@JsonProperty("has_api_key") boolean hasApiKey,
@JsonProperty("health") LLMProviderHealth health) {
this.name = name;
this.type = type;
this.enabled = enabled;
this.priority = priority;
this.weight = weight;
this.hasApiKey = hasApiKey;
this.health = health;
}

public String getName() {
return name;
}

public String getType() {
return type;
}

public boolean isEnabled() {
return enabled;
}

public int getPriority() {
return priority;
}

public int getWeight() {
return weight;
}

@JsonProperty("has_api_key")
public boolean hasApiKey() {
return hasApiKey;
}

/** Health snapshot; may be null if the platform did not return a health probe. */
public LLMProviderHealth getHealth() {
return health;
}
}
56 changes: 56 additions & 0 deletions src/main/java/com/getaxonflow/sdk/types/LLMProviderHealth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2025 AxonFlow
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getaxonflow.sdk.types;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Health snapshot for a registered LLM provider, returned inside an {@link
* LLMProvider} record from {@code GET /api/v1/llm-providers}.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class LLMProviderHealth {

private final String status;
private final String message;
private final String lastChecked;

public LLMProviderHealth(
@JsonProperty("status") String status,
@JsonProperty("message") String message,
@JsonProperty("last_checked") String lastChecked) {
this.status = status;
this.message = message;
this.lastChecked = lastChecked;
}

/** Coarse health label: {@code "healthy"}, {@code "unhealthy"}, or {@code "unknown"}. */
public String getStatus() {
return status;
}

/** Optional human-readable detail (e.g. {@code "billing exceeded"}); may be null. */
public String getMessage() {
return message;
}

/** ISO 8601 timestamp of the last health probe; may be null. */
@JsonProperty("last_checked")
public String getLastChecked() {
return lastChecked;
}
}
75 changes: 75 additions & 0 deletions src/test/java/com/getaxonflow/sdk/AxonFlowTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,81 @@ void orchestratorHealthCheckAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntim
assertThat(health.isHealthy()).isTrue();
}

// ========================================================================
// LLM Providers
// ========================================================================

@Test
@DisplayName("listLLMProviders should return providers with health snapshot")
void listLLMProvidersShouldReturnProvidersWithHealth() {
stubFor(
get(urlEqualTo("/api/v1/llm-providers"))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(
"{\"providers\":[" +
"{\"name\":\"anthropic\",\"type\":\"anthropic\",\"enabled\":true,\"has_api_key\":true,\"health\":{\"status\":\"healthy\",\"message\":\"provider is operational\",\"last_checked\":\"2026-04-28T08:45:12Z\"}}," +
"{\"name\":\"openai\",\"type\":\"openai\",\"enabled\":true,\"has_api_key\":true,\"health\":{\"status\":\"unhealthy\",\"message\":\"billing exceeded\"}}" +
"]}")));

List<LLMProvider> providers = axonflow.listLLMProviders();

assertThat(providers).hasSize(2);
assertThat(providers.get(0).getName()).isEqualTo("anthropic");
assertThat(providers.get(0).getHealth().getStatus()).isEqualTo("healthy");
assertThat(providers.get(1).getName()).isEqualTo("openai");
assertThat(providers.get(1).getHealth().getStatus()).isEqualTo("unhealthy");
assertThat(providers.get(1).getHealth().getMessage()).isEqualTo("billing exceeded");
}

@Test
@DisplayName("listLLMProviders with type filter passes query string")
void listLLMProvidersWithTypeFilterPassesQueryString() {
stubFor(
get(urlEqualTo("/api/v1/llm-providers?type=anthropic"))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"providers\":[]}")));

List<LLMProvider> providers = axonflow.listLLMProviders("anthropic", null);
assertThat(providers).isEmpty();
}

@Test
@DisplayName("listLLMProviders with enabled filter passes query string")
void listLLMProvidersWithEnabledFilterPassesQueryString() {
stubFor(
get(urlEqualTo("/api/v1/llm-providers?enabled=false"))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"providers\":[]}")));

List<LLMProvider> providers = axonflow.listLLMProviders(null, false);
assertThat(providers).isEmpty();
}

@Test
@DisplayName("listLLMProvidersAsync returns a CompletableFuture")
void listLLMProvidersAsyncShouldReturnFuture() throws Exception {
stubFor(
get(urlEqualTo("/api/v1/llm-providers"))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"providers\":[]}")));

CompletableFuture<List<LLMProvider>> future = axonflow.listLLMProvidersAsync();
List<LLMProvider> providers = future.get();
assertThat(providers).isEmpty();
}

// ========================================================================
// MCP Connectors
// ========================================================================
Expand Down
Loading