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.
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
- Gateway Integration
- With trace (optional)
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"}]}'
Span span = tracer.spanBuilder("gateway_call").setSpanKind(SpanKind.CLIENT).startSpan();
try (Scope scope = span.makeCurrent()) {
String traceparent = "00-" + span.getSpanContext().getTraceId() + "-" + span.getSpanContext().getSpanId() + "-01";
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("traceparent", traceparent)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload.toString()))
.build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
} finally {
span.end();
}
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
- Anthropic
- Gemini
- Groq / OpenRouter / Ollama
Uses the official anthropic-java SDK. .apiKey(...) carries the provider key - gateway auth goes through a separate X-Gateway-Key header via .putHeader(...).
- Gateway Integration
- With trace (optional)
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());
Span span = tracer.spanBuilder("anthropic_call").setSpanKind(SpanKind.CLIENT).startSpan();
try (Scope scope = span.makeCurrent()) {
String traceparent = "00-" + span.getSpanContext().getTraceId() + "-" + span.getSpanContext().getSpanId() + "-01";
AnthropicClient client = AnthropicOkHttpClient.builder()
.baseUrl(gatewayUrl + "/anthropic")
.apiKey(anthropicApiKey)
.putHeader("X-Gateway-Key", gatewayKey)
.putHeader("traceparent", traceparent)
.build();
client.messages().create(MessageCreateParams.builder()
.model("claude-3-5-haiku-20241022")
.maxTokens(256)
.addUserMessage("hello")
.build());
} finally {
span.end();
}
Uses the official google-genai SDK. Same split as Anthropic: .apiKey(...) carries the provider key (x-goog-api-key), gateway auth is a separate header on HttpOptions.
- Gateway Integration
- With trace (optional)
import com.google.genai.Client;
import com.google.genai.types.Content;
import com.google.genai.types.GenerateContentResponse;
import com.google.genai.types.HttpOptions;
import com.google.genai.types.Part;
import java.util.List;
import java.util.Map;
Client client = Client.builder()
.apiKey(geminiApiKey)
.httpOptions(HttpOptions.builder()
.baseUrl(gatewayUrl + "/gemini")
.headers(Map.of("X-Gateway-Key", gatewayKey))
.build())
.build();
Content userTurn = Content.fromParts(Part.fromText("hello"));
GenerateContentResponse response = client.models.generateContent("gemini-flash-lite-latest", List.of(userTurn), null);
Span span = tracer.spanBuilder("gemini_call").setSpanKind(SpanKind.CLIENT).startSpan();
try (Scope scope = span.makeCurrent()) {
String traceparent = "00-" + span.getSpanContext().getTraceId() + "-" + span.getSpanContext().getSpanId() + "-01";
Client client = Client.builder()
.apiKey(geminiApiKey)
.httpOptions(HttpOptions.builder()
.baseUrl(gatewayUrl + "/gemini")
.headers(Map.of("X-Gateway-Key", gatewayKey, "traceparent", traceparent))
.build())
.build();
Content userTurn = Content.fromParts(Part.fromText("hello"));
GenerateContentResponse response = client.models.generateContent("gemini-flash-lite-latest", List.of(userTurn), null);
} finally {
span.end();
}
These are OpenAI-compatible through the gateway - use the official openai-java SDK pointed at a different route and provider key.
Without trace:
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
OpenAIClient client = OpenAIOkHttpClient.builder()
.baseUrl(gatewayUrl + "/groq")
.apiKey(gatewayKey)
.putHeader("X-Provider-Key", groqApiKey)
.build();
With trace, add .putHeader("traceparent", traceparent) the same way as the Anthropic/Gemini tabs above.
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.
- LangChain4j
- Quarkus LangChain4j
- Google ADK-Java
.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?");
A genuinely distinct integration shape from plain LangChain4j: declarative config properties + a CDI-injected @RegisterAiService, not an imperative builder call. There's no config property for custom headers on this extension's OpenAiConfig - the real escape hatch is a CDI bean implementing ModelBuilderCustomizer, which gets handed the raw LangChain4j builder before it's built.
// Assistant.java
import io.quarkiverse.langchain4j.RegisterAiService;
@RegisterAiService(tools = WeatherTool.class)
public interface Assistant {
String chat(String userMessage);
}
// WeatherTool.java
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class WeatherTool {
@Tool("Get the current weather for a city")
public String getWeather(@P("the city name") String city) {
return city + ": 72F, sunny";
}
}
// HeaderCustomizer.java - injects X-Provider-Key, since no config property exists for it
import dev.langchain4j.model.openai.OpenAiChatModel;
import io.quarkiverse.langchain4j.ModelBuilderCustomizer;
import jakarta.inject.Singleton;
@Singleton
public class HeaderCustomizer implements ModelBuilderCustomizer<OpenAiChatModel.OpenAiChatModelBuilder> {
@Override
public void customize(OpenAiChatModel.OpenAiChatModelBuilder builder) {
builder.customHeaders(Map.of("X-Provider-Key", System.getenv("GROQ_API_KEY")));
}
}
# application.properties - resolved at build time, so pull real values from
# your deployment's env/secrets, not a runtime dotenv read
quarkus.langchain4j.openai.base-url=${GATEWAY_URL}/groq
quarkus.langchain4j.openai.api-key=${GATEWAY_KEY}
quarkus.langchain4j.openai.chat-model.model-name=llama-3.3-70b-versatile
Gemini only, matching every sibling lab's ADK finding.
import com.google.adk.agents.LlmAgent;
import com.google.adk.agents.RunConfig;
import com.google.adk.models.Gemini;
import com.google.adk.runner.InMemoryRunner;
import com.google.adk.tools.FunctionTool;
import com.google.genai.Client;
import com.google.genai.types.Content;
import com.google.genai.types.HttpOptions;
import com.google.genai.types.Part;
public class WeatherTool {
public Map<String, String> getWeather(String city) {
return Map.of("result", city + ": 72F, sunny");
}
}
Client client = Client.builder()
.apiKey(geminiApiKey)
.httpOptions(HttpOptions.builder()
.baseUrl(gatewayUrl + "/gemini")
.headers(Map.of("X-Gateway-Key", gatewayKey, "traceparent", traceparent))
.build())
.build();
Gemini gemini = new Gemini("gemini-flash-lite-latest", client);
LlmAgent agent = LlmAgent.builder()
.name("weather_agent")
.model(gemini)
.instruction("You are a helpful weather assistant. Use the getWeather tool when asked about weather.")
.tools(FunctionTool.create(new WeatherTool(), "getWeather"))
.build();
InMemoryRunner runner = new InMemoryRunner(agent, "my-app");
var session = runner.sessionService().createSession("my-app", "user-1").blockingGet();
Content userTurn = Content.fromParts(Part.fromText("What's the weather in Paris?"));
var events = runner.runAsync("user-1", session.id(), userTurn, RunConfig.builder().build()).toList().blockingGet();
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
- OpenAI SDK Integration - the reference JavaScript/TypeScript walkthrough.
- Other Providers - full provider/URL-segment reference.
- Gateway Authentication - key precedence and header details.
- Trace Context - how
traceparentcorrelates observations.