Skip to main content

Java Integration

Also available in: Python · PHP · Node.js · Go · .NET

The "gateway base URL" is the address of your Observra Gateway (https://gateway.observra.in) - your app sends requests here instead of directly to OpenAI, Anthropic, or whichever provider you use. The "gateway key" is a per-Application credential you generate in the Observra Dashboard; it authenticates your app to the gateway and is separate from your provider's own API key, which you still keep and send yourself. See Quickstart and Gateway Authentication if you haven't set these up yet.

Try it now

Paste in a gateway URL, your gateway key, and a provider key to send a real request straight from this page - no code required yet.

Loading playground...

Environment variables

GATEWAY_URL
GATEWAY_KEY obs_...
OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY / GROQ_API_KEY / OPENROUTER_API_KEY / OLLAMA_API_KEY

Maven dependencies:

<dependency>
<groupId>com.anthropic</groupId>
<artifactId>anthropic-java</artifactId>
<version>2.52.0</version>
</dependency>
<dependency>
<groupId>com.google.genai</groupId>
<artifactId>google-genai</artifactId>
<version>1.64.0</version>
</dependency>

Agent framework dependencies (only add what you use): dev.langchain4j:langchain4j-open-ai (LangChain4j), io.quarkiverse.langchain4j:quarkus-langchain4j-openai (Quarkus LangChain4j), com.google.adk:google-adk - official (ADK-Java).

Optional: distributed tracing

Uses the OpenTelemetry Java SDK to generate a real traceparent header (see Trace Context) from a real span, not a hand-built random ID:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;

Span span = tracer.spanBuilder("gateway_call").setSpanKind(SpanKind.CLIENT).startSpan();
try (Scope scope = span.makeCurrent()) {
String traceId = span.getSpanContext().getTraceId();
String spanId = span.getSpanContext().getSpanId();
String traceparent = "00-" + traceId + "-" + spanId + "-01";
// ... make the gateway call, attaching traceparent as a header
} finally {
span.end();
}

Raw HTTP request

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.json.JSONArray;
import org.json.JSONObject;

HttpClient http = HttpClient.newHttpClient();
JSONObject payload = new JSONObject()
.put("model", "gpt-4o-mini")
.put("messages", new JSONArray().put(new JSONObject().put("role", "user").put("content", "hello")));

HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(gatewayUrl + "/openai/chat/completions"))
.header("Authorization", "Bearer " + gatewayKey)
.header("X-Provider-Key", openaiApiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload.toString()))
.build();

HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
curl -X POST "${GATEWAY_URL}/openai/chat/completions" \
-H "Authorization: Bearer ${GATEWAY_KEY}" \
-H "X-Provider-Key: ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'

Every provider in Other Providers works the same way - swap the URL segment (/openai/anthropic, /groq, /openrouter, /ollama, ...) and the X-Provider-Key value.

Native SDKs

Uses the official anthropic-java SDK. .apiKey(...) carries the provider key - gateway auth goes through a separate X-Gateway-Key header via .putHeader(...).

import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.messages.MessageCreateParams;

AnthropicClient client = AnthropicOkHttpClient.builder()
.baseUrl(gatewayUrl + "/anthropic")
.apiKey(anthropicApiKey)
.putHeader("X-Gateway-Key", gatewayKey)
.build();

client.messages().create(MessageCreateParams.builder()
.model("claude-3-5-haiku-20241022")
.maxTokens(256)
.addUserMessage("hello")
.build());

Tool calling (native SDK)

The Gemini example below completes the full round-trip - model requests a tool, the app runs it locally, and the result is sent back as a follow-up call so the model can answer with it:

import com.google.genai.types.FunctionCall;
import com.google.genai.types.FunctionDeclaration;
import com.google.genai.types.GenerateContentConfig;
import com.google.genai.types.Schema;
import com.google.genai.types.Tool;
import java.util.List;

FunctionDeclaration weatherFn = FunctionDeclaration.builder()
.name("get_weather")
.description("Get the current weather for a city")
.parameters(Schema.builder()
.type("OBJECT")
.properties(Map.of("city", Schema.builder().type("STRING").build()))
.required("city")
.build())
.build();
GenerateContentConfig config = GenerateContentConfig.builder()
.tools(Tool.builder().functionDeclarations(weatherFn).build())
.build();

Content userTurn = Content.fromParts(Part.fromText("What's the weather in Paris right now? Use the tool."));
GenerateContentResponse response = client.models.generateContent("gemini-flash-lite-latest", List.of(userTurn), config);

List<FunctionCall> calls = response.functionCalls();
if (!calls.isEmpty()) {
FunctionCall call = calls.get(0);
String city = (String) call.args().orElse(Map.of()).getOrDefault("city", "Paris");
Content modelTurn = Content.fromParts(Part.fromFunctionCall(call.name().orElse("get_weather"), call.args().orElse(Map.of())));
Content toolResultTurn = Content.fromParts(Part.fromFunctionResponse(
call.name().orElse("get_weather"), Map.of("result", city + ": 72F, sunny")));

GenerateContentResponse followUp = client.models.generateContent(
"gemini-flash-lite-latest", List.of(userTurn, modelTurn, toolResultTurn), config);
System.out.println(followUp.text());
}

Agent frameworks & SDKs

More advanced: these frameworks wrap the SDKs above. Each still points its underlying model client at the gateway route.

.baseUrl(...) + .customHeaders(...) on the model builder, tool-calling via AiServices and @Tool-annotated methods - the framework handles the tool round-trip for you.

import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;

interface Assistant {
String chat(String userMessage);
}

class WeatherTools {
@Tool("Get the current weather for a city")
String getWeather(@P("the city name") String city) {
return city + ": 72F, sunny";
}
}

ChatModel model = OpenAiChatModel.builder()
.baseUrl(gatewayUrl + "/openai")
.apiKey(gatewayKey)
.modelName("gpt-4o-mini")
.customHeaders(Map.of("X-Provider-Key", openaiApiKey, "traceparent", traceparent))
.build();

Assistant assistant = AiServices.builder(Assistant.class).chatModel(model).tools(new WeatherTools()).build();
String reply = assistant.chat("What's the weather in Paris?");

Every framework tab above adds tracing the same way: start a span, derive traceparent from it as shown in the tracing section, and add it alongside X-Provider-Key/X-Gateway-Key in that tab's headers map/builder call.

Spring AI was tested against the gateway and hit a real NoClassDefFoundError on com.fasterxml.jackson.annotation.JsonSerializeAs (a Jackson version conflict pulled in transitively) - not shown here until that's resolved.

Next steps