Skip to content

Commit 27efb3e

Browse files
feat(client): add listLLMProviders() — closes provider-listing parity gap (#145)
1 parent 07a2f26 commit 27efb3e

5 files changed

Lines changed: 272 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **`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()`.
13+
1014
## [6.1.0] - 2026-04-25 — Plugin Batch 1 explainability fields on MCP responses
1115

1216
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.

src/main/java/com/getaxonflow/sdk/AxonFlow.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,59 @@ public CompletableFuture<RollbackPlanResponse> rollbackPlanAsync(
17431743
// MCP Connectors
17441744
// ========================================================================
17451745

1746+
/**
1747+
* Lists configured LLM providers and their per-provider health snapshot.
1748+
*
1749+
* <p>Calls {@code GET /api/v1/llm-providers}. Mirrors the Python SDK's {@code
1750+
* list_providers()}, the Go SDK's {@code ListProviders()}, and the TypeScript
1751+
* SDK's {@code listProviders()}.
1752+
*
1753+
* @return list of configured providers
1754+
*/
1755+
public List<LLMProvider> listLLMProviders() {
1756+
return listLLMProviders(null, null);
1757+
}
1758+
1759+
/**
1760+
* Lists configured LLM providers, optionally filtered by type and/or enabled flag.
1761+
*
1762+
* @param type filter by provider type (e.g. {@code "openai"}, {@code "anthropic"}); null for no
1763+
* filter
1764+
* @param enabled filter by the enabled boolean; null for no filter
1765+
* @return list of matching providers
1766+
*/
1767+
public List<LLMProvider> listLLMProviders(String type, Boolean enabled) {
1768+
return retryExecutor.execute(
1769+
() -> {
1770+
StringBuilder path = new StringBuilder("/api/v1/llm-providers");
1771+
boolean hasQuery = false;
1772+
if (type != null && !type.isEmpty()) {
1773+
path.append('?').append("type=").append(type);
1774+
hasQuery = true;
1775+
}
1776+
if (enabled != null) {
1777+
path.append(hasQuery ? '&' : '?').append("enabled=").append(enabled);
1778+
}
1779+
Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null);
1780+
try (Response response = httpClient.newCall(httpRequest).execute()) {
1781+
JsonNode node = parseResponseNode(response);
1782+
JsonNode providers = node.has("providers") ? node.get("providers") : node;
1783+
return objectMapper.convertValue(
1784+
providers, new TypeReference<List<LLMProvider>>() {});
1785+
}
1786+
},
1787+
"listLLMProviders");
1788+
}
1789+
1790+
/**
1791+
* Asynchronously lists configured LLM providers.
1792+
*
1793+
* @return a future containing the list of providers
1794+
*/
1795+
public CompletableFuture<List<LLMProvider>> listLLMProvidersAsync() {
1796+
return CompletableFuture.supplyAsync(this::listLLMProviders, asyncExecutor);
1797+
}
1798+
17461799
/**
17471800
* Lists available MCP connectors.
17481801
*
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2025 AxonFlow
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.getaxonflow.sdk.types;
17+
18+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
21+
/**
22+
* A registered LLM provider returned by {@code GET /api/v1/llm-providers}.
23+
*
24+
* <p>Mirrors {@code LLMProvider} in the Python and Go SDKs and the {@code LLMProvider}
25+
* TypeScript interface.
26+
*/
27+
@JsonIgnoreProperties(ignoreUnknown = true)
28+
public final class LLMProvider {
29+
30+
private final String name;
31+
private final String type;
32+
private final boolean enabled;
33+
private final int priority;
34+
private final int weight;
35+
private final boolean hasApiKey;
36+
private final LLMProviderHealth health;
37+
38+
public LLMProvider(
39+
@JsonProperty("name") String name,
40+
@JsonProperty("type") String type,
41+
@JsonProperty("enabled") boolean enabled,
42+
@JsonProperty("priority") int priority,
43+
@JsonProperty("weight") int weight,
44+
@JsonProperty("has_api_key") boolean hasApiKey,
45+
@JsonProperty("health") LLMProviderHealth health) {
46+
this.name = name;
47+
this.type = type;
48+
this.enabled = enabled;
49+
this.priority = priority;
50+
this.weight = weight;
51+
this.hasApiKey = hasApiKey;
52+
this.health = health;
53+
}
54+
55+
public String getName() {
56+
return name;
57+
}
58+
59+
public String getType() {
60+
return type;
61+
}
62+
63+
public boolean isEnabled() {
64+
return enabled;
65+
}
66+
67+
public int getPriority() {
68+
return priority;
69+
}
70+
71+
public int getWeight() {
72+
return weight;
73+
}
74+
75+
@JsonProperty("has_api_key")
76+
public boolean hasApiKey() {
77+
return hasApiKey;
78+
}
79+
80+
/** Health snapshot; may be null if the platform did not return a health probe. */
81+
public LLMProviderHealth getHealth() {
82+
return health;
83+
}
84+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2025 AxonFlow
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.getaxonflow.sdk.types;
17+
18+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
21+
/**
22+
* Health snapshot for a registered LLM provider, returned inside an {@link
23+
* LLMProvider} record from {@code GET /api/v1/llm-providers}.
24+
*/
25+
@JsonIgnoreProperties(ignoreUnknown = true)
26+
public final class LLMProviderHealth {
27+
28+
private final String status;
29+
private final String message;
30+
private final String lastChecked;
31+
32+
public LLMProviderHealth(
33+
@JsonProperty("status") String status,
34+
@JsonProperty("message") String message,
35+
@JsonProperty("last_checked") String lastChecked) {
36+
this.status = status;
37+
this.message = message;
38+
this.lastChecked = lastChecked;
39+
}
40+
41+
/** Coarse health label: {@code "healthy"}, {@code "unhealthy"}, or {@code "unknown"}. */
42+
public String getStatus() {
43+
return status;
44+
}
45+
46+
/** Optional human-readable detail (e.g. {@code "billing exceeded"}); may be null. */
47+
public String getMessage() {
48+
return message;
49+
}
50+
51+
/** ISO 8601 timestamp of the last health probe; may be null. */
52+
@JsonProperty("last_checked")
53+
public String getLastChecked() {
54+
return lastChecked;
55+
}
56+
}

src/test/java/com/getaxonflow/sdk/AxonFlowTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,81 @@ void orchestratorHealthCheckAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntim
571571
assertThat(health.isHealthy()).isTrue();
572572
}
573573

574+
// ========================================================================
575+
// LLM Providers
576+
// ========================================================================
577+
578+
@Test
579+
@DisplayName("listLLMProviders should return providers with health snapshot")
580+
void listLLMProvidersShouldReturnProvidersWithHealth() {
581+
stubFor(
582+
get(urlEqualTo("/api/v1/llm-providers"))
583+
.willReturn(
584+
aResponse()
585+
.withStatus(200)
586+
.withHeader("Content-Type", "application/json")
587+
.withBody(
588+
"{\"providers\":[" +
589+
"{\"name\":\"anthropic\",\"type\":\"anthropic\",\"enabled\":true,\"has_api_key\":true,\"health\":{\"status\":\"healthy\",\"message\":\"provider is operational\",\"last_checked\":\"2026-04-28T08:45:12Z\"}}," +
590+
"{\"name\":\"openai\",\"type\":\"openai\",\"enabled\":true,\"has_api_key\":true,\"health\":{\"status\":\"unhealthy\",\"message\":\"billing exceeded\"}}" +
591+
"]}")));
592+
593+
List<LLMProvider> providers = axonflow.listLLMProviders();
594+
595+
assertThat(providers).hasSize(2);
596+
assertThat(providers.get(0).getName()).isEqualTo("anthropic");
597+
assertThat(providers.get(0).getHealth().getStatus()).isEqualTo("healthy");
598+
assertThat(providers.get(1).getName()).isEqualTo("openai");
599+
assertThat(providers.get(1).getHealth().getStatus()).isEqualTo("unhealthy");
600+
assertThat(providers.get(1).getHealth().getMessage()).isEqualTo("billing exceeded");
601+
}
602+
603+
@Test
604+
@DisplayName("listLLMProviders with type filter passes query string")
605+
void listLLMProvidersWithTypeFilterPassesQueryString() {
606+
stubFor(
607+
get(urlEqualTo("/api/v1/llm-providers?type=anthropic"))
608+
.willReturn(
609+
aResponse()
610+
.withStatus(200)
611+
.withHeader("Content-Type", "application/json")
612+
.withBody("{\"providers\":[]}")));
613+
614+
List<LLMProvider> providers = axonflow.listLLMProviders("anthropic", null);
615+
assertThat(providers).isEmpty();
616+
}
617+
618+
@Test
619+
@DisplayName("listLLMProviders with enabled filter passes query string")
620+
void listLLMProvidersWithEnabledFilterPassesQueryString() {
621+
stubFor(
622+
get(urlEqualTo("/api/v1/llm-providers?enabled=false"))
623+
.willReturn(
624+
aResponse()
625+
.withStatus(200)
626+
.withHeader("Content-Type", "application/json")
627+
.withBody("{\"providers\":[]}")));
628+
629+
List<LLMProvider> providers = axonflow.listLLMProviders(null, false);
630+
assertThat(providers).isEmpty();
631+
}
632+
633+
@Test
634+
@DisplayName("listLLMProvidersAsync returns a CompletableFuture")
635+
void listLLMProvidersAsyncShouldReturnFuture() throws Exception {
636+
stubFor(
637+
get(urlEqualTo("/api/v1/llm-providers"))
638+
.willReturn(
639+
aResponse()
640+
.withStatus(200)
641+
.withHeader("Content-Type", "application/json")
642+
.withBody("{\"providers\":[]}")));
643+
644+
CompletableFuture<List<LLMProvider>> future = axonflow.listLLMProvidersAsync();
645+
List<LLMProvider> providers = future.get();
646+
assertThat(providers).isEmpty();
647+
}
648+
574649
// ========================================================================
575650
// MCP Connectors
576651
// ========================================================================

0 commit comments

Comments
 (0)