diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ac333..a826e25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index 49eb311..3bef471 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -1743,6 +1743,59 @@ public CompletableFuture rollbackPlanAsync( // MCP Connectors // ======================================================================== + /** + * Lists configured LLM providers and their per-provider health snapshot. + * + *

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 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 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>() {}); + } + }, + "listLLMProviders"); + } + + /** + * Asynchronously lists configured LLM providers. + * + * @return a future containing the list of providers + */ + public CompletableFuture> listLLMProvidersAsync() { + return CompletableFuture.supplyAsync(this::listLLMProviders, asyncExecutor); + } + /** * Lists available MCP connectors. * diff --git a/src/main/java/com/getaxonflow/sdk/types/LLMProvider.java b/src/main/java/com/getaxonflow/sdk/types/LLMProvider.java new file mode 100644 index 0000000..749eeae --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/types/LLMProvider.java @@ -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}. + * + *

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; + } +} diff --git a/src/main/java/com/getaxonflow/sdk/types/LLMProviderHealth.java b/src/main/java/com/getaxonflow/sdk/types/LLMProviderHealth.java new file mode 100644 index 0000000..d1b13a9 --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/types/LLMProviderHealth.java @@ -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; + } +} diff --git a/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java b/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java index 8da4f7c..43ac504 100644 --- a/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java +++ b/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java @@ -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 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 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 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> future = axonflow.listLLMProvidersAsync(); + List providers = future.get(); + assertThat(providers).isEmpty(); + } + // ======================================================================== // MCP Connectors // ========================================================================