Skip to content

Commit 938b5dd

Browse files
fix: use smart defaults for Gateway Mode clientId (#67)
Replace requireCredentials() with getEffectiveClientId() that returns "community" as default when clientId is not configured. This enables zero-config usage for community/self-hosted deployments while still supporting enterprise deployments with explicit credentials. Gateway Mode methods (getPolicyApprovedContext, auditLLMCall) now work without requiring credentials to be configured.
1 parent b287934 commit 938b5dd

4 files changed

Lines changed: 58 additions & 34 deletions

File tree

CHANGELOG.md

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

88
## [2.7.1] - 2026-01-25
99

10+
### Changed
11+
12+
- **Gateway Mode smart defaults**: `getPolicyApprovedContext()` and `auditLLMCall()` now use `"community"` as default clientId when not configured, enabling zero-config usage for community/self-hosted deployments
13+
1014
### Fixed
1115

12-
- **Gateway Mode credential enforcement**: `getPolicyApprovedContext()` and `auditLLMCall()` now require credentials (clientId), matching Go/Python SDK behavior for consistency across all SDKs
1316
- **PolicyCategory enum**: Added `PII_SINGAPORE("pii-singapore")` value for Singapore PII detection policies (NRIC, FIN, UEN patterns)
1417
- **proxyLLMCall clientId auto-injection**: Auto-populate `clientId` from config when not explicitly set in `ClientRequest`, matching Go/Python/TypeScript SDK behavior
1518

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

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -294,22 +294,19 @@ public CompletableFuture<HealthStatus> orchestratorHealthCheckAsync() {
294294
public PolicyApprovalResult getPolicyApprovedContext(PolicyApprovalRequest request) {
295295
Objects.requireNonNull(request, "request cannot be null");
296296

297-
// Gateway Mode is an enterprise feature that requires credentials
298-
requireCredentials("Gateway Mode (getPolicyApprovedContext)");
299-
300-
// Auto-populate clientId from config if not set in request (matches Go SDK behavior)
301-
PolicyApprovalRequest effectiveRequest = request;
302-
if ((request.getClientId() == null || request.getClientId().isEmpty())
303-
&& config.getClientId() != null && !config.getClientId().isEmpty()) {
304-
Map<String, Object> ctx = request.getContext();
305-
effectiveRequest = PolicyApprovalRequest.builder()
306-
.userToken(request.getUserToken())
307-
.query(request.getQuery())
308-
.dataSources(request.getDataSources())
309-
.context(ctx == null || ctx.isEmpty() ? null : ctx)
310-
.clientId(config.getClientId())
311-
.build();
312-
}
297+
// Use smart default for clientId - enables zero-config community mode
298+
String effectiveClientId = (request.getClientId() != null && !request.getClientId().isEmpty())
299+
? request.getClientId()
300+
: getEffectiveClientId();
301+
302+
Map<String, Object> ctx = request.getContext();
303+
PolicyApprovalRequest effectiveRequest = PolicyApprovalRequest.builder()
304+
.userToken(request.getUserToken())
305+
.query(request.getQuery())
306+
.dataSources(request.getDataSources())
307+
.context(ctx == null || ctx.isEmpty() ? null : ctx)
308+
.clientId(effectiveClientId)
309+
.build();
313310

314311
final PolicyApprovalRequest finalRequest = effectiveRequest;
315312
return retryExecutor.execute(() -> {
@@ -363,11 +360,32 @@ public CompletableFuture<PolicyApprovalResult> getPolicyApprovedContextAsync(Pol
363360
public AuditResult auditLLMCall(AuditOptions options) {
364361
Objects.requireNonNull(options, "options cannot be null");
365362

366-
// Gateway Mode is an enterprise feature that requires credentials
367-
requireCredentials("Gateway Mode (auditLLMCall)");
363+
// Use smart default for clientId - enables zero-config community mode
364+
String effectiveClientId = (options.getClientId() != null && !options.getClientId().isEmpty())
365+
? options.getClientId()
366+
: getEffectiveClientId();
367+
368+
// Create effective options with the smart default clientId
369+
AuditOptions.Builder builder = AuditOptions.builder()
370+
.contextId(options.getContextId())
371+
.clientId(effectiveClientId)
372+
.responseSummary(options.getResponseSummary())
373+
.provider(options.getProvider())
374+
.model(options.getModel())
375+
.tokenUsage(options.getTokenUsage())
376+
.metadata(options.getMetadata())
377+
.success(options.getSuccess())
378+
.errorMessage(options.getErrorMessage());
379+
380+
// Handle null latencyMs (builder takes primitive long)
381+
if (options.getLatencyMs() != null) {
382+
builder.latencyMs(options.getLatencyMs());
383+
}
384+
385+
AuditOptions effectiveOptions = builder.build();
368386

369387
return retryExecutor.execute(() -> {
370-
Request httpRequest = buildRequest("POST", "/api/audit/llm-call", options);
388+
Request httpRequest = buildRequest("POST", "/api/audit/llm-call", effectiveOptions);
371389
try (Response response = httpClient.newCall(httpRequest).execute()) {
372390
return parseResponse(response, AuditResult.class);
373391
}
@@ -1952,16 +1970,17 @@ private void addAuthHeaders(Request.Builder builder) {
19521970

19531971
/**
19541972
* Requires credentials for enterprise features.
1973+
* Get the effective clientId, using smart default for community mode.
1974+
*
1975+
* <p>Returns the configured clientId if set, otherwise returns "community"
1976+
* as a smart default. This enables zero-config usage for community/self-hosted
1977+
* deployments while still supporting enterprise deployments with explicit credentials.
19551978
*
1956-
* @param feature the feature name for error message
1957-
* @throws AuthenticationException if clientId is not configured
1979+
* @return the clientId to use in requests
19581980
*/
1959-
private void requireCredentials(String feature) {
1960-
if (!config.hasCredentials()) {
1961-
throw new AuthenticationException(
1962-
feature + " requires clientId. Set clientId in config (clientSecret is optional for community mode)."
1963-
);
1964-
}
1981+
private String getEffectiveClientId() {
1982+
String clientId = config.getClientId();
1983+
return (clientId != null && !clientId.isEmpty()) ? clientId : "community";
19651984
}
19661985

19671986
private void addTenantIdHeader(Request.Builder builder) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public final class AuditOptions {
7878

7979
private AuditOptions(Builder builder) {
8080
this.contextId = Objects.requireNonNull(builder.contextId, "contextId cannot be null");
81-
this.clientId = Objects.requireNonNull(builder.clientId, "clientId cannot be null");
81+
this.clientId = builder.clientId; // Optional - SDK will use smart default if null
8282
this.responseSummary = builder.responseSummary;
8383
this.provider = builder.provider;
8484
this.model = builder.model;

src/test/java/com/getaxonflow/sdk/types/MoreTypesTest.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -411,12 +411,14 @@ void shouldFailWhenContextIdIsNull() {
411411
}
412412

413413
@Test
414-
@DisplayName("should fail when clientId is null")
415-
void shouldFailWhenClientIdIsNull() {
416-
assertThatThrownBy(() -> AuditOptions.builder()
414+
@DisplayName("should allow clientId to be null for smart defaults")
415+
void shouldAllowClientIdToBeNull() {
416+
// clientId can be null - SDK will use smart default "community"
417+
AuditOptions options = AuditOptions.builder()
417418
.contextId("ctx")
418-
.build())
419-
.isInstanceOf(NullPointerException.class);
419+
.build();
420+
assertThat(options.getContextId()).isEqualTo("ctx");
421+
assertThat(options.getClientId()).isNull();
420422
}
421423

422424
@Test

0 commit comments

Comments
 (0)