Skip to main content

Java and Spring Boot Integration

Use Java's HTTP client against the gateway's documented Chat Completions route. A Spring Boot service can register the same class as a bean without depending on a version-specific AI framework adapter.

Prerequisites

  • Complete the Quickstart and keep the gateway running.
  • Create a client API token for the application.
  • Use Java 17 or later. Spring Boot already includes Jackson; a plain Java application must add Jackson Databind if it uses the example below.

Set application configuration from secrets or environment variables:

export KEEPTRUSTS_GATEWAY_URL="http://127.0.0.1:41002/v1"
export KEEPTRUSTS_REQUEST_TOKEN="kt_..."
export KEEPTRUSTS_MODEL="gpt-5.4-mini"

Create a governed client

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Map;

public final class GovernedChatService {
private final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
private final ObjectMapper json;
private final URI completionsUri;
private final String token;
private final String model;

public GovernedChatService(
ObjectMapper json,
String gatewayUrl,
String token,
String model) {
this.json = json;
this.completionsUri = URI.create(
gatewayUrl.replaceAll("/+$", "") + "/chat/completions");
this.token = token;
this.model = model;
}

public String complete(String input) throws Exception {
var payload = Map.of(
"model", model,
"messages", List.of(Map.of("role", "user", "content", input))
);

var request = HttpRequest.newBuilder(completionsUri)
.timeout(Duration.ofSeconds(60))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(payload)))
.build();

var response = http.send(request, HttpResponse.BodyHandlers.ofString());
JsonNode body = json.readTree(response.body());
if (response.statusCode() == 400
&& "content_policy_violation".equals(
body.path("error").path("code").asText())) {
throw new IllegalStateException("Request blocked by policy");
}
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IllegalStateException(
"Gateway returned " + response.statusCode() + ": " + response.body());
}

return body.path("choices").path(0).path("message").path("content").asText();
}
}

Construct this service from Spring configuration and register it as a bean:

import org.springframework.context.annotation.Bean;

@Bean
GovernedChatService governedChatService(ObjectMapper json) {
return new GovernedChatService(
json,
requireEnv("KEEPTRUSTS_GATEWAY_URL"),
requireEnv("KEEPTRUSTS_REQUEST_TOKEN"),
requireEnv("KEEPTRUSTS_MODEL")
);
}

private static String requireEnv(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException(name + " is required");
}
return value;
}

The application token is not the upstream provider credential, and the model name must match a configured target returned by the gateway.

Treat health and authorization separately

  • GET /healthz checks gateway process health.
  • Authenticated GET /v1/models verifies the client token and available model catalog.

Use the second check for application readiness because it exercises the same authorization boundary as model requests.

Retry carefully

Do not retry a 400 content_policy_violation, 401, or 403. A 429 or 5xx can be transient, but a timeout can occur after an upstream provider accepted the request. Add retries only when the application has a deliberate duplicate-work and duplicate-cost policy.

Verify the governed path

curl -fsS \
-H "Authorization: Bearer ${KEEPTRUSTS_REQUEST_TOKEN}" \
"${KEEPTRUSTS_GATEWAY_URL}/models"

kt events tail --since 10m --follow

Send a representative Java request while the event tail is active. The matching event is the proof that traffic traversed Keeptrusts.

Next steps