Skip to content

Commit cbe13d2

Browse files
fix(types): restore LLMProvider source compatibility for the 7-arg primitive shape (#151)
Code-review finding on PR #148: the new 13-arg boxed constructor replaced the public 7-arg primitive shape, and getPriority() / getWeight() changed from `int` to `Integer`. That's a real breaking change in a PR framed as additive — pre-existing callers either fail to compile (constructor) or now see nullable return values where they expected primitives (accessors). This PR preserves source compatibility while keeping the boxed storage that the wire-shape contract needs: 1. Add a 7-arg primitive constructor that delegates to the new 13-arg one with nulls for the post-PR-#148 optional fields. Marked @deprecated to nudge new callers toward the boxed form. 2. getPriority() and getWeight() return primitive `int` again (null-safe-unbox to 0). Boxed access is via the new getPriorityBoxed() / getWeightBoxed() methods. 3. getEnabled() (which was BRAND NEW in PR #148, returning Boolean) is renamed to getEnabledBoxed() so the JavaBeans-style name doesn't lure callers into `boolean e = p.getEnabled()` and an NPE on null. Pre-PR-#148 had no getEnabled() so this rename has no consumers. 4. Same Boxed-suffix pattern applied to getHasApiKey → getHasApiKeyBoxed for symmetry; primitive `boolean hasApiKey()` was already there from before #148. Two new regression tests pin the source-compat shape: - llmProviderLegacyConstructorPreservesSourceCompat — exercises the 7-arg primitive constructor and confirms primitive-returning accessors round-trip correctly. - llmProviderPrimitiveAccessorsNullSafe — confirms primitive accessors null-safe-unbox to 0 / false when the boxed field is null (which is what Jackson produces when the JSON field is omitted). Existing test that asserted on `p.getEnabled()` updated to `p.getEnabledBoxed()` to match the new naming. Wire-shape baseline refreshed — the renamed getter is not part of the wire contract (Jackson reads via constructor), but the validator records SDK class shape and noticed the rename. No spec drift.
1 parent e3808b1 commit cbe13d2

3 files changed

Lines changed: 159 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
### Fixed
1616

1717
- **`pom.xml`**`mvn verify -DskipUnitTests=true` now actually skips surefire (unit tests). Previously the property was unbound — `-DskipUnitTests` was a no-op flag and unit tests ran redundantly during integration-test invocations. The flag now binds to the surefire `<skipTests>` config; default remains `false`.
18+
- **`LLMProvider` source compatibility** — review-driven follow-up to PR #148. The original PR replaced the public 7-arg primitive constructor (`LLMProvider(name, type, enabled:bool, priority:int, weight:int, hasApiKey:bool, health)`) with a 13-arg boxed constructor and changed `getPriority()` / `getWeight()` from `int` to `Integer`. Pre-existing callers either failed to compile or started seeing nullable return values in what was framed as an additive change. **Restored:** the 7-arg primitive constructor (delegates to the 13-arg one with nulls for the post-PR-#148 optional fields, marked `@Deprecated` to point new callers at the boxed form), and primitive-returning `getPriority()` / `getWeight()` (null-safe-unboxes to 0). Boxed accessors remain available as `getPriorityBoxed()` / `getWeightBoxed()` / `getEnabledBoxed()` / `getHasApiKeyBoxed()` for callers that need to distinguish "explicitly 0" from "field not present". The boxed `getEnabled()` from PR #148 is renamed to `getEnabledBoxed()` (was a brand-new method in #148 with no consumers, safe to rename pre-tag).
1819

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

src/main/java/com/getaxonflow/sdk/types/LLMProvider.java

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.getaxonflow.sdk.types;
1717

18+
import com.fasterxml.jackson.annotation.JsonCreator;
1819
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
1920
import com.fasterxml.jackson.annotation.JsonProperty;
2021
import java.util.Map;
@@ -26,10 +27,19 @@
2627
* populated when the provider config has them set; {@code settings} is a free-form
2728
* provider-specific map.
2829
*
29-
* <p>{@code enabled} and {@code hasApiKey} are typed as {@link Boolean} (boxed) so a
30-
* missing or {@code null} value in the JSON response is distinguishable from the
31-
* explicit boolean values — primitive {@code boolean} would silently default to
32-
* {@code false} and mask whether the field was actually emitted.
30+
* <p><b>Source-compatibility note.</b> Pre-PR-#148 callers wrote {@code new LLMProvider(
31+
* name, type, true, 0, 0, true, health)} (7 args, primitive booleans/ints) and called
32+
* {@code int p = provider.getPriority()} / {@code int w = provider.getWeight()}. The
33+
* 7-arg primitive constructor and the primitive-returning {@code getPriority()} /
34+
* {@code getWeight()} accessors are preserved as a compatibility shim. The 13-arg
35+
* boxed constructor is the Jackson entry point; new optional fields default to null
36+
* via the legacy constructor.
37+
*
38+
* <p>Internal storage is boxed ({@link Boolean} / {@link Integer}) so the SDK can
39+
* faithfully represent fields that were omitted by an older platform. New methods
40+
* exposing the boxed values directly are suffixed with {@code Boxed} (e.g.
41+
* {@link #getPriorityBoxed()}) for callers that need to distinguish "explicitly 0"
42+
* from "field not present".
3343
*/
3444
@JsonIgnoreProperties(ignoreUnknown = true)
3545
public final class LLMProvider {
@@ -48,6 +58,11 @@ public final class LLMProvider {
4858
private final Integer timeoutSeconds;
4959
private final Map<String, Object> settings;
5060

61+
/**
62+
* Full constructor used by Jackson — accepts boxed types so a missing field in the
63+
* JSON response stays null instead of silently becoming {@code false} / {@code 0}.
64+
*/
65+
@JsonCreator
5166
public LLMProvider(
5267
@JsonProperty("name") String name,
5368
@JsonProperty("type") String type,
@@ -77,6 +92,36 @@ public LLMProvider(
7792
this.settings = settings;
7893
}
7994

95+
/**
96+
* Pre-PR-#148 constructor signature — 7 args, primitive {@code boolean} /
97+
* {@code int}. Preserved as a compatibility shim so callers that constructed
98+
* {@code LLMProvider} directly continue to compile. Delegates to the full
99+
* 13-arg constructor with null for the post-PR-#148 optional fields.
100+
*
101+
* @deprecated Prefer the 13-arg constructor when constructing programmatically;
102+
* this overload exists only to preserve compile-time source compatibility for
103+
* pre-existing call sites.
104+
*/
105+
@Deprecated
106+
public LLMProvider(
107+
String name,
108+
String type,
109+
boolean enabled,
110+
int priority,
111+
int weight,
112+
boolean hasApiKey,
113+
LLMProviderHealth health) {
114+
this(
115+
name,
116+
type,
117+
Boolean.valueOf(enabled),
118+
Integer.valueOf(priority),
119+
Integer.valueOf(weight),
120+
Boolean.valueOf(hasApiKey),
121+
health,
122+
null, null, null, null, null, null);
123+
}
124+
80125
public String getName() {
81126
return name;
82127
}
@@ -85,36 +130,75 @@ public String getType() {
85130
return type;
86131
}
87132

88-
/** May be null if the platform omitted the field. */
89-
public Boolean getEnabled() {
133+
/**
134+
* Convenience: returns true if the {@code enabled} field was explicitly set to
135+
* true; false otherwise (including when the field was omitted by the platform).
136+
* Mirrors the pre-PR-#148 primitive-returning accessor.
137+
*/
138+
public boolean isEnabled() {
139+
return Boolean.TRUE.equals(enabled);
140+
}
141+
142+
/**
143+
* Returns the raw boxed {@code enabled} value. May be null if the platform
144+
* omitted the field — use this when you need to distinguish "explicitly false"
145+
* from "not set".
146+
*/
147+
public Boolean getEnabledBoxed() {
90148
return enabled;
91149
}
92150

93-
/** Convenience: true if explicitly enabled, false otherwise (including null). */
94-
public boolean isEnabled() {
95-
return Boolean.TRUE.equals(enabled);
151+
/**
152+
* Convenience: returns the {@code priority} field as a primitive {@code int};
153+
* returns 0 when the field was omitted. Mirrors the pre-PR-#148 primitive-
154+
* returning accessor.
155+
*/
156+
public int getPriority() {
157+
return priority != null ? priority : 0;
96158
}
97159

98-
/** May be null if the platform omitted the field. */
99-
public Integer getPriority() {
160+
/**
161+
* Returns the raw boxed {@code priority}; null when the platform omitted the
162+
* field — use this when you need to distinguish "explicitly 0" from "not set".
163+
*/
164+
public Integer getPriorityBoxed() {
100165
return priority;
101166
}
102167

103-
/** May be null if the platform omitted the field. */
104-
public Integer getWeight() {
105-
return weight;
168+
/**
169+
* Convenience: returns the {@code weight} field as a primitive {@code int};
170+
* returns 0 when the field was omitted. Mirrors the pre-PR-#148 primitive-
171+
* returning accessor.
172+
*/
173+
public int getWeight() {
174+
return weight != null ? weight : 0;
106175
}
107176

108-
/** May be null if the platform omitted the field. */
109-
public Boolean getHasApiKey() {
110-
return hasApiKey;
177+
/**
178+
* Returns the raw boxed {@code weight}; null when the platform omitted the
179+
* field — use this when you need to distinguish "explicitly 0" from "not set".
180+
*/
181+
public Integer getWeightBoxed() {
182+
return weight;
111183
}
112184

113-
/** Convenience: true if has_api_key is explicitly true, false otherwise (including null). */
185+
/**
186+
* Convenience: returns true if {@code has_api_key} was explicitly set to true;
187+
* false otherwise (including when the field was omitted). Mirrors the pre-
188+
* PR-#148 primitive-returning accessor.
189+
*/
114190
public boolean hasApiKey() {
115191
return Boolean.TRUE.equals(hasApiKey);
116192
}
117193

194+
/**
195+
* Returns the raw boxed {@code has_api_key}; null when the platform omitted the
196+
* field — use this when you need to distinguish "explicitly false" from "not set".
197+
*/
198+
public Boolean getHasApiKeyBoxed() {
199+
return hasApiKey;
200+
}
201+
118202
/** Health snapshot; may be null if the platform did not return a health probe. */
119203
public LLMProviderHealth getHealth() {
120204
return health;

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,10 +801,65 @@ void llmProviderEnabledIsBoxed() {
801801
List<LLMProvider> providers = axonflow.listLLMProviders();
802802
assertThat(providers).hasSize(1);
803803
LLMProvider p = providers.get(0);
804-
assertThat(p.getEnabled()).isNull();
804+
// The boxed accessor distinguishes "field not present" from "explicitly false";
805+
// the convenience accessor returns false for both.
806+
assertThat(p.getEnabledBoxed()).isNull();
805807
assertThat(p.isEnabled()).isFalse();
806808
}
807809

810+
@Test
811+
@DisplayName("LLMProvider preserves pre-PR-#148 7-arg primitive constructor for source compat")
812+
@SuppressWarnings("deprecation")
813+
void llmProviderLegacyConstructorPreservesSourceCompat() {
814+
// Pre-PR-#148 callers wrote `new LLMProvider(name, type, true, 1, 2, true, null)`
815+
// with primitive booleans/ints. The 7-arg overload preserves that signature so
816+
// those call sites continue to compile after the boxed-types change.
817+
LLMProvider p = new LLMProvider("anthropic", "anthropic", true, 1, 2, true, null);
818+
819+
assertThat(p.getName()).isEqualTo("anthropic");
820+
assertThat(p.getType()).isEqualTo("anthropic");
821+
assertThat(p.isEnabled()).isTrue();
822+
assertThat(p.hasApiKey()).isTrue();
823+
// Primitive accessors return the unboxed value.
824+
assertThat(p.getPriority()).isEqualTo(1);
825+
assertThat(p.getWeight()).isEqualTo(2);
826+
// Boxed accessors expose the same value, also non-null when set via the
827+
// primitive constructor.
828+
assertThat(p.getPriorityBoxed()).isEqualTo(1);
829+
assertThat(p.getWeightBoxed()).isEqualTo(2);
830+
assertThat(p.getEnabledBoxed()).isTrue();
831+
assertThat(p.getHasApiKeyBoxed()).isTrue();
832+
// Post-PR-#148 fields default to null when constructed via the legacy overload.
833+
assertThat(p.getEndpoint()).isNull();
834+
assertThat(p.getModel()).isNull();
835+
assertThat(p.getRegion()).isNull();
836+
assertThat(p.getRateLimit()).isNull();
837+
assertThat(p.getTimeoutSeconds()).isNull();
838+
assertThat(p.getSettings()).isNull();
839+
assertThat(p.getHealth()).isNull();
840+
}
841+
842+
@Test
843+
@DisplayName("LLMProvider primitive accessors return 0/false when boxed field is null")
844+
void llmProviderPrimitiveAccessorsNullSafe() {
845+
// Construct via the boxed constructor with explicit nulls — Jackson's
846+
// omit-field path produces this same state.
847+
LLMProvider p = new LLMProvider(
848+
"x", "openai", null, null, null, null, null,
849+
null, null, null, null, null, null);
850+
851+
// Primitive accessors null-safe-unbox to 0 / false; boxed accessors expose
852+
// the actual null so callers can distinguish "explicitly 0" from "not set".
853+
assertThat(p.getPriority()).isEqualTo(0);
854+
assertThat(p.getWeight()).isEqualTo(0);
855+
assertThat(p.isEnabled()).isFalse();
856+
assertThat(p.hasApiKey()).isFalse();
857+
assertThat(p.getPriorityBoxed()).isNull();
858+
assertThat(p.getWeightBoxed()).isNull();
859+
assertThat(p.getEnabledBoxed()).isNull();
860+
assertThat(p.getHasApiKeyBoxed()).isNull();
861+
}
862+
808863
// ========================================================================
809864
// MCP Connectors
810865
// ========================================================================

0 commit comments

Comments
 (0)