Last reviewed: November 5, 2026 (Asia/Shanghai); sources fetched November 5 UTC.
Direct answer
Build a CometAPI Java client around one reusable java.net.http.HttpClient. Serialize a small application-owned request, send it to the documented Chat Completions operation, collect the response through a byte-limited subscriber, and validate the completion before returning text. Keep the credential and model in trusted process configuration.
The CometAPI chat reference
documents POST /v1/chat/completions, bearer authentication, model, messages, and the completion response. Oracle’s Java 21 HttpClient reference
documents the reusable client and asynchronous exchange. Those contracts let a Java application implement a narrow text method without a provider-specific SDK.
The example below accepts exactly one completed assistant text choice. HTTP 200, valid JSON, and a nonempty string are separate checks. A refusal, tool call, or length-limited answer needs another application result type.
Who this is for
Use this tutorial for a Java 21 backend, worker, or command-line application that wants direct ownership of HTTP behavior. You need a Java 21 SDK, a Maven project configured to compile for Java 21, and a CometAPI credential supplied through your local secret mechanism.
This implementation uses the standard HTTP client and Gson for JSON. If your application already uses Spring AI, the existing Spring AI integration guide covers that framework’s configuration and abstractions. Here, the application constructs and checks the wire request itself.
The preparation environment had no Java SDK. The code was reviewed against the primary references but was not compiled or executed here. The verification matrix below is an acceptance plan, and no authenticated CometAPI request or deployment is claimed.
Key takeaways
- Reuse an application-owned
HttpClientso calls can share connection resources. - Keep the full operation URI fixed and attach bearer authentication to each request.
- Reject excessive response bytes before handing them to a full-body collector.
- Wait on a future whose body subscriber completes after collection, then attempt cancellation on timeout or interruption.
- Parse strict JSON and apply a separate completion acceptance policy.
- Keep fixed failure codes and numeric HTTP status values out of transcripts and raw provider errors.
Set up the Java project
Add the dependency documented by the Gson project to your Maven project:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.15.0</version>
</dependency>
Use your project’s reviewed compiler and dependency configuration. The checked project README lists this version and supports Java versions older than Java 21; the example’s Java 21 requirement comes from its chosen HTTP-client lifecycle and language baseline.
Set COMETAPI_MODEL to an exact current model ID suitable for Chat Completions. The CometAPI model reference
documents public GET /api/models discovery without an authorization header. Inspect model type, capabilities, and endpoint hints; a catalog entry alone does not establish your account’s access or compatibility with every optional parameter.
Inject COMETAPI_KEY through a protected runtime mechanism. Do not put its value into Java source, Maven properties committed to a repository, command examples, or diagnostic output. The method below accepts only a prompt; callers cannot submit their own endpoint, credential, or model through that argument.
Implement the bounded client
Save this as src/main/java/CometJavaDemo.java in a project using the default package, or add your normal package declaration and directory structure.
import com.google.gson.*;
import java.io.IOException;
import java.net.URI;
import java.net.http.*;
import java.nio.ByteBuffer;
import java.nio.charset.*;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.Flow;
public final class CometJavaDemo {
private static final URI ENDPOINT =
URI.create("https://api.cometapi.com/v1/chat/completions");
private static final int MAX_BODY_BYTES = 1_048_576;
private static final Gson JSON = new GsonBuilder()
.setStrictness(Strictness.STRICT).create();
static final class ChatFault extends RuntimeException {
final String code;
ChatFault(String code) {
super(code);
this.code = code;
}
}
static final class LimitedBody
implements HttpResponse.BodySubscriber<byte[]> {
private final HttpResponse.BodySubscriber<byte[]> delegate =
HttpResponse.BodySubscribers.ofByteArray();
private Flow.Subscription subscription;
private long remaining;
private boolean done;
LimitedBody(long limit) {
if (limit < 1) throw new IllegalArgumentException("invalid_limit");
remaining = limit;
}
public CompletionStage<byte[]> getBody() {
return delegate.getBody();
}
public void onSubscribe(Flow.Subscription value) {
subscription = value;
delegate.onSubscribe(value);
}
public void onNext(List<ByteBuffer> buffers) {
if (done) return;
long size = 0;
for (ByteBuffer buffer : buffers) size += buffer.remaining();
if (size > remaining) {
done = true;
subscription.cancel();
delegate.onError(new IOException("response_too_large"));
return;
}
remaining -= size;
delegate.onNext(buffers);
}
public void onError(Throwable failure) {
if (!done) {
done = true;
delegate.onError(failure);
}
}
public void onComplete() {
if (!done) {
done = true;
delegate.onComplete();
}
}
}
static String complete(HttpClient http, String key,
String model, String prompt) {
if (key == null || key.isBlank() || model == null || model.isBlank())
throw new ChatFault("missing_configuration");
if (prompt == null || prompt.isBlank() || prompt.length() > 4000)
throw new ChatFault("invalid_prompt");
String payload = JSON.toJson(Map.of(
"model", model,
"stream", false,
"messages", List.of(Map.of("role", "user", "content", prompt))
));
HttpRequest request = HttpRequest.newBuilder(ENDPOINT)
.header("Authorization", "Bearer " + key)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
payload, StandardCharsets.UTF_8))
.build();
CompletableFuture<HttpResponse<byte[]>> pending = http.sendAsync(
request, info -> new LimitedBody(MAX_BODY_BYTES));
HttpResponse<byte[]> response;
try {
response = pending.get(30, TimeUnit.SECONDS);
} catch (TimeoutException failure) {
pending.cancel(true);
throw new ChatFault("exchange_deadline");
} catch (InterruptedException failure) {
pending.cancel(true);
Thread.currentThread().interrupt();
throw new ChatFault("caller_interrupted");
} catch (ExecutionException failure) {
throw new ChatFault("exchange_failed");
}
if (response.statusCode() != 200)
throw new ChatFault("cometapi_http_" + response.statusCode());
String mediaType = response.headers()
.firstValue("content-type").orElse("").split(";", 2)[0].trim();
if (!mediaType.equalsIgnoreCase("application/json"))
throw new ChatFault("unexpected_media_type");
return acceptedText(response.body());
}
static String string(JsonObject object, String name) {
JsonElement value = object.get(name);
return value != null && value.isJsonPrimitive()
&& value.getAsJsonPrimitive().isString()
? value.getAsString() : null;
}
static boolean present(JsonObject object, String name) {
return object.has(name) && !object.get(name).isJsonNull();
}
static String acceptedText(byte[] body) {
try {
String decoded = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(body)).toString();
JsonObject root = JSON.fromJson(decoded, JsonObject.class);
if (root == null || !"chat.completion".equals(string(root, "object")))
throw new ChatFault("unaccepted_completion");
JsonArray choices = root.getAsJsonArray("choices");
if (choices == null || choices.size() != 1)
throw new ChatFault("unaccepted_completion");
JsonObject choice = choices.get(0).getAsJsonObject();
JsonObject message = choice.getAsJsonObject("message");
if (message == null)
throw new ChatFault("unaccepted_completion");
String text = string(message, "content");
if (!"stop".equals(string(choice, "finish_reason"))
|| !"assistant".equals(string(message, "role"))
|| present(message, "refusal")
|| present(message, "tool_calls")
|| text == null || text.isBlank())
throw new ChatFault("unaccepted_completion");
return text;
} catch (ChatFault failure) {
throw failure;
} catch (RuntimeException | CharacterCodingException failure) {
throw new ChatFault("invalid_json_or_shape");
}
}
public static void main(String[] args) {
try (HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NEVER)
.build()) {
String answer = complete(http,
System.getenv("COMETAPI_KEY"),
System.getenv("COMETAPI_MODEL"),
"Explain idempotency in one sentence.");
System.out.println(answer);
} catch (Exception failure) {
System.err.println(failure instanceof ChatFault fault
? fault.code : "local_failure");
System.exit(1);
}
}
}
The request uses Gson serialization, so quotes and newlines in a prompt remain JSON data. It sends one user message and explicitly disables streaming. It adds no model-specific sampling, reasoning, or generation-limit options; verify those against the chosen model before extending the request.
The Gson troubleshooting guide
explains that legacy lenient parsing can accept malformed JSON and recommends Strictness.STRICT. That setting governs JSON syntax. The checks in acceptedText separately govern the result this application accepts. Extra metadata can remain present, while the consumed fields must have the expected types and values.
The application rejects any non-null tool_calls field, including an empty array. That is a deliberately strict text-only policy. It does not imply that every such response is invalid under the API contract. Broaden the result type and fixtures deliberately if your selected route returns a different acceptable representation.
Understand body ownership and deadlines
Oracle’s BodySubscriber contract
describes an ordered flow of lists of ByteBuffer objects. LimitedBody checks each batch’s remaining bytes before forwarding that batch to the standard byte-array collector. A batch that crosses the ceiling cancels the subscription and completes the collector exceptionally.
The one-mebibyte ceiling bounds bytes accepted by that collector. Incoming transport buffers already exist, the collector and final array can add allocations, and decoding and parsing create more objects. Concurrent calls multiply those costs. Treat this as a response policy rather than a total process-memory guarantee. The implementation counts received bytes and does not trust Content-Length to enforce the limit.
The same collector handles success and error responses. Consequently, an oversized error body can become exchange_failed before the application reaches its numeric HTTP-status branch. This is intentional: retaining the resource boundary takes precedence over collecting an arbitrary error page for diagnosis.
The future returned by this full-body subscriber completes after body collection. Waiting up to thirty seconds therefore includes a body that stalls after its headers arrive. The five-second connection setting is a separate transport control. Both values are example application choices, not CometAPI latency promises.
The timed wait does not cover request construction beforehand or synchronous decoding and parsing afterward. It also does not guarantee that cleanup finishes within thirty seconds. Oracle documents cancellation as an attempt to release exchange resources and explicitly warns that a request may still reach the server. Preserve that uncertainty when deciding whether another generation is safe.
In a service, create the HTTP client at application startup and close it during shutdown. Do not run the command-line example’s create-and-close scope for every incoming request. Oracle documents that separate client instances generally do not share their connection pools. Check the JVM’s proxy configuration for the intended deployment while preserving normal TLS certificate verification.
Contract details to verify
| Boundary | Application policy | Required check |
|---|---|---|
| Destination | Fixed HTTPS Chat Completions URI; redirects disabled | Exact method and complete URI in a synthetic exchange |
| Input | Nonblank prompt, at most 4,000 Java string units | Invalid input produces zero outbound dispatches |
| Body | At most 1,048,576 accepted bytes | Exact-limit acceptance and one-byte-over rejection |
| Time | Thirty-second wait on the response future | A response that stalls after headers triggers cancellation |
| JSON | UTF-8 decoding and strict JSON syntax | Invalid encoding and malformed JSON fail with fixed codes |
| Result | One assistant text choice with stop and no refusal or tool-call field | Every unsupported outcome is rejected |
| Logging | Fixed code, safe correlation metadata, numeric status when available | No credential, prompt, raw body, or exception cause is emitted |
Java String.length() counts UTF-16 code units. The prompt limit is neither a token count nor a spending cap. Likewise, limiting bytes collected locally does not prove that upstream generation or charging stopped. Add model-specific generation budgets and workload admission controls as separate application policies.
Happy-path operator workflow
First compile the project with your Java 21 toolchain and run synthetic checks. In a conventional Maven project, mvn compile dependency:copy-dependencies prepares the classes and dependencies. On a POSIX shell, java -cp 'target/classes:target/dependency/*' CometJavaDemo runs the default-package example and initiates a live request when runtime credentials are present.
Before that live run, inject the key privately and refresh the exact model configuration. Use the harmless built-in prompt, then require HTTP 200, the JSON media type, successful bounded collection, and the completed-text acceptance verdict. The command prints the accepted answer as its intended output; a service should send it only to the authorized response consumer and exclude it from routine operational logs.
Retain the Java and Gson versions, model identifier, endpoint family, status when available, elapsed time, and acceptance verdict. Compare the Python HTTPX client guide for another runtime’s transport boundary, while keeping Java’s subscriber and future lifecycle checks specific to this implementation.
Error-path operator workflow
Use a controlled HTTP test server or injected test transport with synthetic credentials. Keep any test endpoint override inside test code so ordinary callers cannot redirect authenticated requests.
| Fixture | Required observation |
|---|---|
| Valid completed-text JSON | Exact fixture text is returned |
| Blank or over-limit prompt | invalid_prompt; zero outbound dispatches |
| HTTP 302 | Numeric HTTP failure; no redirect destination receives a request |
| Small HTTP 401, 429, or 500 response | Fixed status code; no body appears in the error |
| HTTP 200 with HTML | unexpected_media_type |
| Invalid UTF-8, malformed JSON, or incompatible field types | invalid_json_or_shape |
Refusal, tool call, empty text, multiple choices, or length | No successful text result |
| Complete body exactly at the ceiling | Collection completes, then normal parsing applies |
| Body one byte above the ceiling | Subscription is canceled; future fails before parsing |
| Headers followed by a stalled body | Timed wait fails and cancellation is attempted |
| Calling thread interrupted | Cancellation is attempted and the interruption flag is restored |
Test the subscriber directly with a recording Flow.Subscription to verify cancellation and with several batches to verify cumulative counting. Then exercise it through the actual HTTP client: a subscriber-only check cannot establish request construction, redirects, or stalled-body behavior.
Stop after a failure and classify it before repeating the request. There is no application retry loop in this example. That does not promise exactly one wire transmission or exactly-once remote execution. An uncertain timeout needs a bounded replay policy, not an immediate series of new generations.
Failure modes
A new client is created for each service request. Move transport ownership to application startup and shutdown so calls can reuse the same client resources.
The body limit is applied after deserialization. The full body has already been collected. Keep the check in the subscriber before forwarding excess bytes to the collector.
A custom subscriber stops requesting data without cancellation. Oracle warns that failure to consume or cancel can retain exchange resources and stall orderly shutdown. Preserve the delegate’s demand handling and cancel on rejection.
HTTP success is returned as application success. Keep media-type, JSON, envelope, and completion-state checks. Supporting tools or refusals requires an explicit product branch.
A timeout triggers an unconditional retry. Local cancellation provides no proof that generation never started. Carry the caller’s remaining deadline and permitted attempts into any later retry design.
Debugging exposes a key or transcript. Inspect surrounding HTTP diagnostics and logging middleware as well as this method. Fixed local codes cannot prevent another layer from dumping headers or bodies.
Sources checked
Six primary public references were fetched successfully on November 5, 2026 UTC:
- CometAPI: Create a chat completion — operation, authentication, request and response fields, streaming distinction, and finish reasons.
- CometAPI: List available models — public model discovery, IDs, capabilities, and endpoint hints.
- Oracle: Java 21 HttpClient — connection reuse, asynchronous exchanges, lifecycle ownership, and cancellation limitations.
- Oracle: Java 21 BodySubscriber — ordered buffers, completion timing, demand, cancellation, and resource release.
- Gson project documentation — JSON-library purpose, Maven dependency, version 2.15.0, and Java requirements.
- Gson Troubleshooting Guide — malformed-input handling and explicit strict JSON parsing.
These maintained and versioned references support an evergreen implementation. The local limits, completion policy, test matrix, and troubleshooting order are recommendations derived from those contracts. They are not provider defaults or recorded test outcomes.
FAQ
Does Java include a JSON parser in HttpClient?
The HTTP client supplies transport and body handling. This example adds Gson for serialization and parsing, then checks the application result explicitly.
Can I replace the subscriber with an InputStream?
That changes ownership and completion timing. Oracle documents that a streaming body can become available before consumption finishes. Add byte and time limits around the later reads and close or exhaust the stream on every path.
Does strict JSON parsing validate the answer’s meaning?
It validates syntax. The completion policy checks the response representation and state. Business accuracy, authorization, safe rendering, and tool permissions remain application responsibilities.
Can this method consume SSE?
It sends stream: false and expects a single JSON document. Streaming requires a separate incremental event parser and completion policy.
Reader next step
Compile the example, run the synthetic matrix, and verify the cumulative-byte and stalled-body cases through your actual Java client. Then make one authorized staging request with a current model and keep its observation separate from fixture results.
If you need an account or credential for that check, Start with CometAPI . Keep the first integration text-only, retain its nonsecret configuration, and add model options only with corresponding source checks and failure fixtures.