Back to blog

API & SDKGuide

First request in Java with openai-java: base URL, key, usage

A runnable Java 8+ program: openai-java pointed at CheapTokens, one chat completion, and the answer, finish_reason and token usage printed.

By CheapTokens TeamPublished 4 min read

Java code that points openai-java at the CheapTokens API
On this page
  1. Add the dependency
  2. Set the key and base URL outside the code
  3. The program
  4. Set the values explicitly with the builder
  5. Three errors you'll meet first
  6. What one request costs

To call CheapTokens from Java, keep the official openai-java SDK and change three things: the base URL (https://api.cheaptokens.dev/v1), the API key, and the model id, passed as a plain String. The program below sends one chat completion and prints the answer, the finish_reason and the token usage. It needs Java 8 or later.

If your team already has a Spring service that talks to OpenAI, this is the whole migration: no new client, no new request classes.

Add the dependency#

The SDK's README (github.com/openai/openai-java, checked 4 November 2026) shows version 4.99.0, group com.openai, artifact openai-java. The README doesn't print a release date, so look at the repository for the latest version before you copy this.

Add openai-java to your build

Maven

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

Gradle

kotlin
implementation("com.openai:openai-java:4.99.0")

The framework-neutral artifacts run on Java 8+, so an older corporate JDK is not a blocker.

Set the key and base URL outside the code#

The SDK reads two environment variables: OPENAI_API_KEY and OPENAI_BASE_URL. OpenAIOkHttpClient.fromEnv() picks both up, so the key never lands in your source files. (System properties openai.apiKey and openai.baseUrl also work and take precedence over the variables.)

bash
export OPENAI_API_KEY="your-cheaptokens-key"
export OPENAI_BASE_URL="https://api.cheaptokens.dev/v1"

The default base URL of the SDK is https://api.openai.com/v1. Skip the second line and your request goes there, with a CheapTokens key it doesn't know. That is one way to get a 401 on the first run. For where to keep the key and how to keep it out of git, see store an API key safely.

The two environment variables and a run command in a terminal
Key and base URL come from the environment, not from the source file

The program#

Create FirstRequest.java:

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

public class FirstRequest {
  public static void main(String[] args) {
    OpenAIClient client = OpenAIOkHttpClient.fromEnv();

    ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
        .model("deepseek-ai/DeepSeek-V4-Flash-0731")
        .addUserMessage("Explain in two sentences what a token is.")
        .maxCompletionTokens(300L)
        .build();

    ChatCompletion completion = client.chat().completions().create(params);
    ChatCompletion.Choice choice = completion.choices().get(0);

    System.out.println(choice.message().content().orElse("(no text)"));
    System.out.println("finish_reason: " + choice.finishReason());
    completion.usage().ifPresent(u ->
        System.out.println("input " + u.promptTokens()
            + ", output " + u.completionTokens()));
  }
}

A few lines deserve a second look.

  • .model("deepseek-ai/DeepSeek-V4-Flash-0731"). The SDK's README examples use typed constants such as ChatModel.GPT_5_2, but the builder also has a model(String) overload, so any id works. Use the full id from the CheapTokens docs: the short name deepseek-v4-flash-0731 is how prices are labelled on the site; the API wants the full id with the deepseek-ai/ prefix. Not sure of an id? List models with GET /v1/models.
  • .maxCompletionTokens(300L). A ceiling on the answer length, as a long. DeepSeek-V4-Flash allows up to 32,768 output tokens per answer, so 300 here is a choice to keep the demo short, not a limit of the model.
  • content() is an Optional. That's why the program has orElse. A reply made only of tool calls has no text.
  • usage() is an Optional too. ifPresent prints it when the server sends it.

Compile and run it the way your build does (mvn compile exec:java -Dexec.mainClass=FirstRequest, or a JavaExec task in Gradle). You should get the explanation of a token, then a line finish_reason: … with stop in it, then a line with the input and output token counts. If finish_reason says length, the answer hit your 300-token cap and was cut: raise the number (why that happens).

usage() returns prompt, completion and total token counts (promptTokens(), completionTokens(), totalTokens()).

Set the values explicitly with the builder#

Tests, or a service with several clients, sometimes need explicit values. The same client, built by hand:

java
OpenAIClient client = OpenAIOkHttpClient.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .baseUrl("https://api.cheaptokens.dev/v1")
    .build();

Everything after this line, the params and the create call, stays exactly as above. Keep reading the key from the environment even here; a string literal in a .java file ends up in your repository.

Java code, openai-java, the CheapTokens endpoint and the model
Your code talks to the SDK; only the base URL decides who answers

Three errors you'll meet first#

Match the pairs

Match the symptom to the cause

  • 401 Unauthorizedthe key is empty or wrong, or the request went to api.openai.com because OPENAI_BASE_URL is not set
  • 404 Not Foundthe base URL lacks /v1, or the model id doesn't exist
  • finish_reason says lengthmaxCompletionTokens is lower than the answer needs

An empty environment variable is easy to miss in an IDE: a run configuration started from the toolbar may not inherit the variables you exported in the terminal. If the key "is set" but you still get 401, add the variables to the run configuration. More on these codes: API 401 or 404: key, base URL, model id.

What one request costs#

Because you print usage, you can price the request before you scale it. Say your service sends about 200 input tokens and gets 300 back. At today's prices that request costs $0.000033 on DeepSeek-V4-Flash. Input and output are billed together, and in total that is cheaper than OpenRouter. The table repeats it for 1,000 requests across our models:

Model Per request 1,000 requests OpenRouter, 1,000 requests
GLM-5.3-Flash $0.000016 $0.016 $0.0316
DeepSeek-V4-Flash $0.000033 $0.033 $0.056
MiniMax-M2.7 $0.000055 $0.055 $0.66

200 input and 300 output tokens per request. Prices per 1M tokens are current and change together with our tariffs; the OpenRouter column shows reference prices for the same workload.

Now plug in your own traffic. Pull the sliders to the numbers from your first printed usage line:

Cost calculator

A support service with 2,000 requests a day

Input tokens per request: 200 · Output tokens per request: 300 · Requests per day: 2,000

ModelPer dayPer month
GLM-5.3-Flash$0.032$0.96
DeepSeek-V4-Flash$0.066$1.98
MiniMax-M2.7$0.11$3.30

For more room per answer or a different shape of workload, DeepSeek-V4-Flash gives you a 380K context window and one price at any hour of the day; GLM-5.3-Flash (zai-org/GLM-5.3-Flash) is a drop-in replacement in the same program: change the id, nothing else. Current prices for all models are on the pricing page, and the docs show the same request in other languages. Doing this in Go? See the openai-go version.