We use cookies. Cookies and browser storage keep you signed in and remember your preferences. With your permission we also measure how the site is used. You can any time. See our cookie policy.

Java

SDK guides

Java

Use api14 from Java with the openai-java SDK, with setup, chat completion, streaming and error handling examples you can run.

Use the official openai-java library with api14. Set baseUrl on the client builder and pass model ids (for example openai/gpt-5-mini) as plain strings.

Install

Gradle:

implementation("com.openai:openai-java:4.+")

Maven:

<dependency>
  <groupId>com.openai</groupId>
  <artifactId>openai-java</artifactId>
  <version>LATEST</version>
</dependency>

Then set API14_API_KEY=sk-api14-... in your environment.

Configure

import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;

OpenAIClient client = OpenAIOkHttpClient.builder()
    .baseUrl("https://api14.com/api/v1")
    .apiKey(System.getenv("API14_API_KEY"))
    .build();

OpenAIOkHttpClient.fromEnv() reads OPENAI_BASE_URL and OPENAI_API_KEY instead, so you can switch environments without code changes:

export OPENAI_BASE_URL="https://api14.com/api/v1"
export OPENAI_API_KEY="sk-api14-..."

Chat

import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;

ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
    .model("openai/gpt-5-mini")
    .addUserMessage("Explain vector databases in two sentences.")
    .build();

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

Streaming

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

ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
    .model("anthropic/claude-sonnet-4.5")
    .addUserMessage("Write a haiku about APIs.")
    .build();

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

See Streaming.

Tool calling

Describe a tool as a class; the SDK derives its JSON Schema and parses the arguments back into it:

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.openai.models.chat.completions.*;
import java.util.Collection;

@JsonClassDescription("Current weather for a city")
static class GetWeather {
    public String city;
}

ChatCompletionCreateParams.Builder builder = ChatCompletionCreateParams.builder()
    .model("openai/gpt-5-mini")
    .addTool(GetWeather.class)
    .addUserMessage("What's the weather in Istanbul?");

while (true) {
    ChatCompletionMessage message = client.chat().completions().create(builder.build())
        .choices().get(0).message();
    builder.addMessage(message);
    if (message.toolCalls().map(Collection::isEmpty).orElse(true)) {
        message.content().ifPresent(System.out::println);
        break;
    }
    for (ChatCompletionMessageToolCall call : message.toolCalls().get()) {
        GetWeather args = call.asFunction().function().arguments(GetWeather.class);
        builder.addMessage(ChatCompletionToolMessageParam.builder()
            .toolCallId(call.asFunction().id())
            .contentAsJson(getWeather(args.city)) // your function
            .build());
    }
}

The builder keeps the whole conversation: each loop adds the assistant message with its tool calls, then one tool message per call, and sends everything again. Fields of the tool class become the schema's properties; add @JsonPropertyDescription to a field to tell the model what it means. Cap the number of iterations in production code so a model that keeps calling tools cannot loop forever. See Tool calling.

Structured output

Pass a class to responseFormat; the SDK generates a strict JSON Schema from it and parses each choice's content into an instance. Use a model that lists structured_outputs or response_format in its supported_parameters:

static class Person {
    public String name;
    public int born;
    public String city;
}

StructuredChatCompletionCreateParams<Person> params = ChatCompletionCreateParams.builder()
    .model("openai/gpt-5-mini")
    .responseFormat(Person.class)
    .addUserMessage("Extract: Ada Lovelace, born 1815 in London.")
    .build();

client.chat().completions().create(params).choices().stream()
    .flatMap(choice -> choice.message().content().stream())
    .forEach(person -> System.out.println(person.name + " " + person.born));

See Structured outputs.

Image input

import java.util.List;

ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
    .model("google/gemini-2.5-flash")
    .addUserMessageOfArrayOfContentParts(List.of(
        ChatCompletionContentPart.ofText(ChatCompletionContentPartText.builder()
            .text("What is in this picture?").build()),
        ChatCompletionContentPart.ofImageUrl(ChatCompletionContentPartImage.builder()
            .imageUrl(ChatCompletionContentPartImage.ImageUrl.builder()
                .url("https://example.com/photo.jpg") // or a data:image/png;base64,... URL
                .build())
            .build())))
    .build();

See Vision & files.

Extra parameters

Gateway fields without a typed setter (reasoning, provider, models) go through putAdditionalBodyProperty:

import com.openai.core.JsonValue;
import java.util.Map;

ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
    .model("anthropic/claude-sonnet-4.5")
    .addUserMessage("Prove there are infinitely many primes.")
    .putAdditionalBodyProperty("reasoning", JsonValue.from(Map.of("effort", "high")))
    .build();

Errors

Non-2xx responses throw subclasses of OpenAIServiceException, which expose the status and headers:

import com.openai.errors.OpenAIServiceException;

try {
    client.chat().completions().create(params);
} catch (OpenAIServiceException e) {
    if (e.statusCode() == 402) {
        System.err.println("Balance too low, add funds (insufficient_balance)");
    } else if (e.statusCode() == 429) {
        System.err.println("Rate limited, retry after " + e.headers().values("Retry-After") + "s");
    } else {
        throw e;
    }
}

See Errors.

Timeouts and retries

The client retries connection errors, 408, 409, 429 and 5xx twice by default with exponential backoff. Configure the count and timeout on the builder, or per request:

import com.openai.core.RequestOptions;
import java.time.Duration;

OpenAIClient client = OpenAIOkHttpClient.builder()
    .baseUrl("https://api14.com/api/v1")
    .apiKey(System.getenv("API14_API_KEY"))
    .maxRetries(4)
    .timeout(Duration.ofSeconds(60))
    .build();

client.chat().completions().create(params,
    RequestOptions.builder().timeout(Duration.ofMinutes(5)).build());

The builder timeout applies to each attempt. Long reasoning requests can take minutes, so give them a longer per-request timeout or stream them. Retrying does not help with daily_limit_reached or insufficient_balance; catch RateLimitException and check the error code before scheduling another attempt. See Rate limits.

All capabilities

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api14.com/api/v1",
  apiKey: process.env.API14_API_KEY,
});

const result = await client.chat.completions.create({
  "model": "openai/gpt-5-mini",
  "messages": [
    {
      "role": "user",
      "content": "Explain vector databases in two sentences."
    }
  ]
});
console.log(result.choices[0].message.content);