This was the README of lm15 for Java until 2026-09-26. It describes the port as it was written against contract
cfed007(2026-09-11).
The Java port of lm15: one canonical request/response model over every
provider the lm15-contract
names, byte-exact against its corpus. Synchronous, zero runtime
dependencies (the JDK's java.net.http for HTTP and WebSocket), Java 21.
The contract commit this port is built against is in CONTRACT_PIN;
harness/check.py refuses to grade the port against any other commit.
Contract-complete at the pin. Every harness direction is green, with
zero failures and no skips added; the one skip per chat direction is a
corpus gap (openai.computer_use has no canonical request and no golden),
the same skip the reference reports.
| Direction | Contract surface | Result |
|---|---|---|
serde |
spec/types.md, spec/vocabularies.md, spec/invariants.md, docs/serde-rules.md; all 36 kinds | 115 / 0 |
error |
ErrorCode + class hierarchy; normalize_error per provider |
84 / 0 |
auth |
spec/auth.md AUTH-1/2/5/7/8/10 (core, 32) and the three cloud chains, AUTH-11 (cloud, 11) | 43 / 0 |
token |
SigV4 (34 vectors), RS256 JWTs, token exchanges | 43 / 0 |
request |
the four dialects, request side; MAP-5..8, MAP-10; hosts, presets | 366 / 0 (1 skip) |
response |
the four dialects, response side; MAP-1..4, MAP-9 | 302 / 0 (1 skip) |
stream |
SSE decoding, MAP-3/4 coalescing, MAP-9 assembly and its refusal | 40 / 0 |
router |
the three rungs, precedence, unknown_model / ambiguous_model |
22 / 0 |
models |
list_models on every provider |
34 / 0 |
files, batch, cache |
the three surfaces, multipart byte for byte, MAP-11 id escaping | 48 / 0, 41 / 0, 11 / 0 |
generation, video |
image and speech generation, video jobs | 20 / 0, 27 / 0 |
live |
the websocket codec (OpenAI Realtime, Gemini Live) | 24 / 0 |
ingest |
MAP-12: a Chat Completions request body → Request under one preset's spellings; the response door |
160 / 0 |
Beyond the harness: 165 unit and integration tests (mvn test) and
368 independent request comparisons (23 probes × 8 providers × complete /
stream). Of these, 348 match Python; 20 verify documented corrections to its
lost citations and unsupported media. Nothing is skipped: the corrected
results are checked explicitly against MAP-10, and every reference difference
is recorded. See CONFORMANCE.md and
history content for evidence and reproduction.
CI also tests the standalone JAR outside the source checkout, on Linux
(Java 21/25) and macOS (Java 21).
No provider was called from this port: batch jobs, image and video generation, the cloud credential chains, the OAuth refresh wire, the xAI device-code login and the websocket sessions are proven by the pinned lifecycles, vectors and transcripts of the corpus and by scripted-transport tests, not by a receipt from a real server. The first live smoke is the next step.
mvn -B package # unit tests + target/lm15.jar
python3 tools/check_contract.py --contract ../lm15-contract --direction all
python3 tools/differential.py --contract ../lm15-contract \
--python-repo ../lm15-python --verify-documented-fixesThe contract is never copied into this repository: the serde test
(CanonicalTest) replays serde/canonical.json from the sibling checkout
when it is present and skips itself otherwise.
import dev.lm15.*;
import dev.lm15.router.LMRouter;
import dev.lm15.stream.ResponseStream;
import dev.lm15.types.*;
LMRouter router = new LMRouter(); // keys from the environment (AUTH-1)
Request request = Request.builder("groq:openai/gpt-oss-20b") // or "claude-haiku-4-5", "gpt-4.1-mini"
.user("hi")
.config(Config.builder().maxTokens(100).build())
.build();
// One call.
Response response = router.complete(request);
System.out.println(response.text());
// Streamed: text as it arrives, then the same Response `complete` returns.
try (ResponseStream rs = router.responseStream(request)) {
for (String text : rs) System.out.print(text);
Response same = rs.response();
}
// How was it routed? `resolve` is pure: no network, no files, no secrets.
System.out.println(router.resolve("grok-4"));Direct providers use the same credential discovery as the router, or accept an explicit key/value/callback. A callback is invoked once per request, never while constructing the client. Cloud credential exchanges use the configured transport and clock. For example, a provider directly with a key:
ProviderLM lm = OpenAILM.create(System.getenv("OPENAI_API_KEY"));
Response response = lm.complete(Request.builder("gpt-4.1-mini").system("You are terse.").user("Say hello in three words.").build());Every OpenAI-compatible server through the Chat Completions dialect; a compat preset name bundles that server's wire quirks and its address:
ProviderLM ollama = OpenAIChatLM.builder().apiKey("ollama").preset("ollama").build(); // http://localhost:11434/v1FunctionTool weather = new FunctionTool("get_weather", "Get the current weather for a city.",
Json.obj("type", "object", "properties", Json.obj("city", Json.obj("type", "string")), "required", Json.arr("city")));
Request first = Request.builder("gpt-4.1-mini").user("What is the weather in Montreal?").tool(weather)
.config(Config.builder().toolChoice(ToolChoice.REQUIRED.withParallel(false)).build()).build();
Response response = lm.complete(first);
ToolCallPart call = response.toolCalls().get(0);
String result = "Sunny and 22°C in " + call.input().get("city").asString();
List<Message> messages = new ArrayList<>(first.messages());
messages.add(response.message());
messages.add(Message.tool(call.id(), result));
Response answer = lm.complete(first.withMessages(messages).withConfig(Config.builder().toolChoice(ToolChoice.NONE).build()));The schema is written by you (FunctionTool.parameters is JSON Schema);
no derivation from a method signature — the family's stated deviation for
every non-Python port. lm15 never runs the loop for you.
Each names the rule it deviates from (playbooks/port.md rule 8). The wire is not affected unless the entry says so.
wait(WaitOptions)onBatchJob/VideoJob(api-family § Beyond chat): Java reserves the no-argumentwait()on every object, so the handle'swaittakes an options value (WaitOptions.DEFAULT), withwaitDone()as the no-argument spelling. A deadline past isjava.util.concurrent.TimeoutException(the language's own timeout type, by rule).- Records, not keyword arguments (api-family § Types and serde):
canonical types are Java records; the many-field ones (
Config,Usage,CacheConfig,Request,FileInfo,LiveConfig) carry a builder, which is how a field added later stays "keyword-only" (rule 6).Request.systemis aSystemPrompt(a string or prompt parts); nullable fields are absent whennull, neverOptional. - Serde is
Canonical.toJson(x)/Canonical.xFromJson(json): static functions over oneJsonValuemodel (dev.lm15.json) that keeps1and1.0apart and object key order intact; opaque payloads areJsonObjects and round-trip verbatim. - Validation is
ValidationException(anIllegalArgumentException) whoseprotocolName()isValueErrororTypeError, the native names the reference raises and the vet protocol reports. - Sync only (api-family rule 4):
completeblocks,streamreturns anIteratorthat is alsoAutoCloseable; there is no async mirror. Use a thread or an executor. - Credential provider is the single-method interface (
CredentialProvider), the Go/Rust shape; a plain string is theApiKeyshorthand. - Router rung 0 and catalog discovery: NEVER, as in every non-Python
port — the router takes a data catalog (
RouterConfig.catalog). ProviderProfile/EndpointProfile: never ported (contract decision 2026-09-11); the same facts arepreset(...)+baseUrl(...).- A
provider:prefix onRequest.modelgiven to a direct adapter is removed when it names that adapter's own provider (OpenAILMsendsgpt-5.4foropenai:gpt-5.4), as the Rust port does; the reference sends the string verbatim. The router strips it on every port. - Stored logins yield a
BearerToken(AUTH-2) where the reference coerces the stored string to anApiKey; every subscription door listsbearer, so the wire is identical. The injected provider transport/clock governs cloud exchanges, not the borrowed stores' dedicated OAuth refresh implementation. - History content follows MAP-10 rather than inherited omissions.
Responses preserves assistant images/files; unsupported combinations raise
before sending. Citations retain their title, URL and quote. The documented
differences from Python are tested, not ignored (see
docs/history-content.md). streamreturnsProviderLM.EventStream(anIteratorthat isAutoCloseable) rather than a language-level lazy sequence; closing it releases the connection.ResponseStreamwraps it.
- The loopback OAuth callback listener (AUTH-9's third primitive): no flow this port owns needs one (xAI is device-code); PKCE and RFC 8628 polling are shipped. On demand, per the family decision.
- Streaming a chat
Requestover the Realtime websocket for-realtimemodels (the reference's websocket transport mode): the codec is ported and pinned; the socket driver for that mode is not. aws-event-streamframing (Bedrock Converse, phase 2),aws loginrefresh, Azure Service Fabric managed identity, GCPexternal_accountwith an AWS source: each is a typedNotConfiguredErrornaming the fix, the same gap every port states.surface_dumpreports the canonical record fields and vocabularies by reflection; it is not a module gate (the ratchet runs on the reference).
See PORTING.md for the package map and the conventions a contributor
follows; the vet shim is dev.lm15.vet.Main (java -jar target/lm15.jar).