Java

The official openai-java SDK builds its client from a builder, so routing through the firewall means two builder calls — .baseUrl(...) and .apiKey(...). Your request params, streaming, and response handling are unchanged. The provider key stays in the vault; this JVM only carries the Vulnetix API key.

Install

Maven
<dependency>
  <groupId>com.openai</groupId>
  <artifactId>openai-java</artifactId>
  <version>LATEST</version>
</dependency>
Gradle
implementation("com.openai:openai-java:LATEST")

Configure

import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.ChatModel;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;

String org = System.getenv("VULNETIX_ORG_UUID");

OpenAIClient client = OpenAIOkHttpClient.builder()
    .baseUrl("https://guardrails.vulnetix.com/openai/" + org + "/v1")
    .apiKey(System.getenv("VULNETIX_API_KEY")) // Vulnetix key, not an OpenAI key
    .build();

ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
    .model(ChatModel.GPT_4O_MINI)
    .addUserMessage("Hello")
    .build();

ChatCompletion completion = client.chat().completions().create(params);
System.out.println(completion.choices().get(0).message().content().orElse(""));

Model names pass through verbatim, so for a non-OpenAI provider — where ChatModel has no constant — build one from the string the provider itself uses:

.model(ChatModel.of("llama-3.3-70b-versatile"))   // groq

Streaming

Unchanged. The gateway relays the provider’s SSE stream.

import com.openai.core.http.StreamResponse;
import com.openai.models.chat.completions.ChatCompletionChunk;

try (StreamResponse<ChatCompletionChunk> stream =
        client.chat().completions().createStreaming(params)) {
    stream.stream().forEach(chunk ->
        chunk.choices().forEach(choice ->
            choice.delta().content().ifPresent(System.out::print)));
}

Handling a guardrail block

A request your policy refuses never reaches the provider. It returns an OpenAI-shaped 403, which the SDK throws as com.openai.errors.PermissionDeniedException. Its body() carries the error object, including the Vulnetix blocked_by field.

import com.openai.errors.PermissionDeniedException;

try {
    ChatCompletion completion = client.chat().completions().create(params);
} catch (PermissionDeniedException e) {   // HTTP 403
    // e.statusCode() == 403
    // e.body() carries: code, message, blocked_by, violations
    throw new IllegalStateException("AI firewall refused this request: " + e.body(), e);
}

Switch on the code field in the body to tell your organisation refused this apart from the provider refused this:

codeMeaning
request_blockedA content guardrail matched. blocked_by names the rule; violations lists every rule that matched.
provider_deniedPolicy does not allow this provider.
model_denied / model_not_allowedPolicy does not allow this model.
provider_key_missingNo provider key in the vault for this provider.

Every SDK exception extends com.openai.errors.OpenAIServiceException, so catch that if you want one handler for all HTTP failures and branch on statusCode().

Note Redaction is silent — nothing is thrown. A pii_redact rule rewrites the matched spans in your prompt to the literal [REDACTED] and forwards the request. You get an ordinary ChatCompletion back, about redacted text.

Tool / function calling

Tools are forwarded untouched: the tools you add to the params and the tool calls you get back are the provider’s own. Guardrails inspect the request you send, which includes the tool-result messages you add before the next call — so a tool that returns a secret is something the firewall can still block or redact.

Gotchas

  • .apiKey(...) takes the Vulnetix key. A provider key here produces a 401.
  • The client reads OPENAI_API_KEY from the environment if you omit .apiKey(...). If that variable still holds a real OpenAI key on your machine, be explicit — otherwise you will authenticate to the gateway with the wrong credential.
  • .baseUrl(...) must include the /v1 suffix.
  • If you are on Spring AI rather than the raw SDK, configure it with properties instead — see Spring AI.