Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
## [Unreleased]

### Added
- **`InvalidRequestException`** (`net.ladenthin.llama.exception`, a `LlamaException`): the JNI layer now
throws it wherever llama.cpp's own HTTP server answers `400` (`invalid_request_error`) -- a body that is
not valid JSON, a missing `"prompt"` / `"messages"` / `"input_prefix"`, an empty embedding input, an
unknown `encoding_format`, a parameter outside its hard limits, a prompt over the context size, and a
`std::invalid_argument` or `common_json_error` out of tokenization, prompt formatting or the
`/v1/systemone` handler (the same rule as upstream's `ex_wrapper`). Everything else stays a plain
`LlamaException`. A `catch (LlamaException)` keeps catching both.
- **`RouterModel.getContextLength()`** (llama.cpp b11538, #30228): the context length a model was
trained with, which the router now reads offline from the GGUF for `GET /models` (`context_length`,
`<arch>.context_length`) -- known before the first load, like the modalities; `0` from a server before
b11538 or for a file without the key.
- **CI proves HTTPS in both directions on the release assets.** The fat-jar smokes (Linux x86-64 /
aarch64, Windows x86-64 / arm64) and the macOS smoke each start the server behind a self-signed
certificate (`--ssl-key-file` / `--ssl-cert-file`; a plain-HTTP request to the port must be refused)
Expand Down Expand Up @@ -60,6 +71,10 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
core's; `check-natives.py` fails when they differ.

### Changed
- **The message of a rejected request is the bare reason** (`"prompt" must not be empty`), no longer the
JSON object `{"code":400,"message":…,"type":"invalid_request_error"}` the old `throw_invalid_request`
wrapped it in: the type is the exception class now (`InvalidRequestException`), as it is for every
other `LlamaException`.
- **`NativeServer` binds port 9931 when no `--port` is given** (llama.cpp b11521,
[#30159](https://github.com/ggml-org/llama.cpp/pull/30159) moved the server default off 8080), in classic and
attach mode alike; `NativeServer.getPort()` reports 9931 accordingly. `OpenAiCompatServer` keeps its own default
Expand All @@ -69,6 +84,12 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
in `build.sh` opts back in). With CUDA 13.4 and sccache 0.18.0 the nvcc launcher failed on the first `.cu`
files in two confirmed runs (`Missing "cubin" file output`, `Compiler killed by signal 126`), and the
uncached retry that followed made those jobs slower than a cold build; the C/C++ objects stay cached.
- **Upgraded the pinned llama.cpp from b11534 to b11538**, one reviewed step. All eleven patches apply
unchanged and every drop-check still finds its defect. In the range: the router's `GET /models` carries
`context_length` per model (#30228, the `RouterModel` getter above; `common_get_decision_type(fname)`
became `common_get_gguf_info()`, which no project code called), the embedding `get_rows` is built after
the inputs (#30160), a CUDA rounding mismatch between CPU and GPU under MSVC is fixed (#30229), and
`llama-bench` respects `-fitc` (#28331). Per-step record: `docs/history/llama-cpp-breaking-changes.md`.
- **CI: the three dockcross images are pinned by digest** (`android-arm64`, `android-x86_64`,
`manylinux_2_28-x64`, all tag `20260712-79e54f9`), as the manylinux aarch64 image already was.
- **Upgraded the pinned llama.cpp from b11529 to b11534**, in 3 reviewed steps, each ending at a tag. No
Expand Down Expand Up @@ -283,6 +304,14 @@ from version 5.0.0 onward. Pre-fork releases (`1.x`–`4.2.0`) were authored by
4.4.7 (none of the files the agent's carried JLine fixes touch changed between 4.4.6 and 4.4.7).

### Fixed
- **`OpenAiCompatServer` answers `400`, not `500`, for a request the native layer rejects** -- an empty
`input` array for `/v1/embeddings`, a `/v1/completions` body without a `prompt`, a malformed body, a
message the chat template cannot take, a prompt over the context size -- on every surface (OpenAI,
Ollama, Anthropic, Responses; as an `invalid_request_error` SSE event once a stream has started).
Upstream's server has answered these with `400` since b11337 (#29060); the Java transport reported them
as `server_error`, because the JNI layer threw the same `LlamaException` for a rejected request and a
failed inference. The server now maps `InvalidRequestException` like the `IllegalArgumentException` of
its own validation, without matching on message text; a plain `LlamaException` stays `500`.
- **HTTPS works out of every desktop natives jar, and the macOS dylib no longer needs Homebrew's OpenSSL.**
`jllama` compiled cpp-httplib itself, without SSL, next to the SSL-enabled copy upstream's `llama-common`
(the download code) links -- two incompatible copies of the same classes in one link, and which one the
Expand Down
35 changes: 27 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

Java bindings for [llama.cpp](https://github.com/ggerganov/llama.cpp) via JNI, providing a high-level API for LLM inference in Java. The Java layer communicates with a native C++ library through JNI.

Current llama.cpp pinned version: **b11534**
Current llama.cpp pinned version: **b11538**

## Natives jars: one directory per backend (`.github/natives.csv`)

Expand Down Expand Up @@ -897,7 +897,7 @@ needs no extra step here, `build-webui` re-reads the tag and rebuilds the matchi
ships no UI):
```bash
# needs node/npm + network for the asset build; the embed step is plain cmake -P
git clone --depth 1 --branch b11534 https://github.com/ggml-org/llama.cpp /tmp/lc
git clone --depth 1 --branch b11538 https://github.com/ggml-org/llama.cpp /tmp/lc
( cd /tmp/lc/tools/ui && npm ci && npm run build )
mkdir -p webui-generated /tmp/ui-gen
cmake -DUI_SOURCE_DIR=/tmp/lc/tools/ui -DUI_BINARY_DIR=/tmp/ui-gen \
Expand Down Expand Up @@ -937,7 +937,7 @@ cache lives in **Depot Cache** over sccache's **WebDAV** backend:
- `SCCACHE_WEBDAV_TOKEN: ${{ secrets.DEPOT_TOKEN }}` — a Depot **organization** token, stored
as the repo secret **`DEPOT_TOKEN`**.

Because `sccache` is **content-addressed** and llama.cpp is pinned (`GIT_TAG b11534`), the
Because `sccache` is **content-addressed** and llama.cpp is pinned (`GIT_TAG b11538`), the
~280 upstream object files are byte-identical every run, so a warm cache recompiles only the
*changed* files. Depot's cache is **shared across all branches** (unlike GitHub's
per-branch `actions/cache`), so every branch builds incrementally; a `b<nnnn>` version bump
Expand Down Expand Up @@ -1710,7 +1710,7 @@ root from the loader's own package (which moved to `…loader`) and (b)
`jllama.cpp` still `FindClass`-ing the old flat paths — and neither was visible
to a local `mvn test` (model tests skipped) or to the pure-Java unit tests.
**When you move a Java class the JNI layer references by name** (`LlamaModel`
[root], `exception.LlamaException`, `value.LogLevel`, `args.LogFormat`,
[root], `exception.LlamaException`, `exception.InvalidRequestException`, `value.LogLevel`, `args.LogFormat`,
`callback.LoadProgressCallback`), update the matching `FindClass` / `"L…;"`
signature string in `src/main/cpp/jllama.cpp` and keep the native-resource root
anchored at `net/ladenthin/llama` in `LlamaLoader.NATIVE_RESOURCE_BASE` (it must
Expand Down Expand Up @@ -1866,7 +1866,7 @@ The project C++ helpers follow a strict semantic split:
Functions: `get_result_error_message`, `results_to_json`, `rerank_results_to_json`,
`parse_encoding_format`, `extract_embedding_prompt`, `is_infill_request`,
`parse_slot_prompt_similarity`, `parse_positive_int_config`, `wrap_stream_chunk`,
`server_metrics_to_json`, `route_error_message`.
`server_metrics_to_json`, `route_error_message`, `get_result_error_code`.

**`log_helpers.hpp`** — Pure log-formatting transforms.
- Input: `ggml_log_level`, message text (`const char*`), an explicit `std::time_t` timestamp.
Expand Down Expand Up @@ -1955,6 +1955,25 @@ rather than a Java exception, and `train_engine.cpp` deliberately keeps its own
and never includes `jni_helpers.hpp`, so its backstop returns an error string to preserve that
contract.

**Two Java classes, chosen by what upstream would answer.** `c_llama_error` (`LlamaException`) is the
backstop and the class for everything that is not the request's fault; `c_llama_invalid_request`
(`InvalidRequestException`, a subclass) is thrown wherever llama.cpp's own HTTP server answers `400`,
so that `OpenAiCompatServer` can answer `400` too without matching on message text (it maps the type
like the `IllegalArgumentException` of its own validation; everything else is `500`). Three helpers
in `jllama.cpp` decide it: `throw_invalid_request` for request *parsing* (an invalid request whatever
the parser threw — upstream's parsers mix `std::invalid_argument` and `std::runtime_error` for the
same kind of defect; also behind `parse_json_params` and `require_json_field`),
`throw_by_exception_type` for upstream code whose exception type carries the verdict
(`std::invalid_argument` and `common_json_error` → 400, else 500 — the rule of upstream's `ex_wrapper`
in `tools/server/server.cpp`; tokenization, `format_prompt_infill`, the `/v1/systemone` handler), and
`error_class_for` for a failed task result (`get_result_error_code` reads the code
`format_error_response` put into it: 400 for `ERROR_TYPE_INVALID_REQUEST` and
`ERROR_TYPE_EXCEED_CONTEXT_SIZE`). A `ThrowNew(c_llama_error, …)` is still right for a missing
capability (`--embedding`, `--reranking`, FIM tokens: upstream answers those `501`) and for a broken
result. Pinned model-free by `OpenAiCompatServerHttpTest` (a backend throwing the typed exception is
`400` on every surface, a plain `LlamaException` `500`) and model-backed by `ErrorHandlingTest` and
the two server integration tests (an empty `input` array, a `/v1/completions` body without a prompt).

**When you add a native method, wrap it.** The guard is not enforced by a test — a new unguarded
entry point is invisible until something throws through it in production.

Expand Down Expand Up @@ -2148,7 +2167,7 @@ ctest --test-dir build --output-on-failure -R "ResultsToJson"
|------|-------|-------|
| `src/test/cpp/test_utils.cpp` | 168 | Upstream helpers: `server_tokens`, `server_grammar_trigger`, `gen_tool_call_id`, `json_value`, `json_get_nested_values`, UTF-8 helpers, `format_response_rerank`, `format_embeddings_response_oaicompat`, `oaicompat_completion_params_parse`, `oaicompat_chat_params_parse`, `are_lora_equal`, `strip_flag_from_argv`, `token_piece_value`, `json_is_array_and_contains_numbers`, `format_oai_sse`, `format_oai_resp_sse`, `format_anthropic_sse`, `parse_lora_request`, `common_chat_parse` over malformed UTF-8 (the `ContentOnlyParseUtf8` guard, which pins upstream #29161's one-U+FFFD-per-invalid-run contract — formerly the guard for the dropped `patches/0011`) |
| `src/test/cpp/test_server.cpp` | 206 | Upstream result types: `server_slot_stats` (the `timings` JSON payload; replaced `result_timings` in b10408), `task_params::to_json()` (incl. `dry_sequence_breakers`, `preserved_tokens`, `timings_per_token`), `completion_token_output`, `server_task_result_cmpl_partial` (non-oaicompat + `to_json_oaicompat` + logprobs + `to_json_oaicompat_chat` + `to_json_anthropic` + dispatcher), `server_task_result_cmpl_final` (non-oaicompat + `to_json_oaicompat` + `to_json_oaicompat_chat` + `to_json_oaicompat_chat_stream` + `to_json_anthropic` + `to_json_anthropic_stream` + tool_calls + dispatcher), `server_task_result_embd`, `server_task_result_rerank`, `server_task_result_metrics` (`to_metrics()` = the `/metrics` Prometheus exposition text; its `to_json()` has been unused since b10519 and returns `json{}` = JSON null), `server_task_result_slots` (`to_json()` = the `/slots` array, fed by the b10519 `SERVER_TASK_TYPE_SLOT_GET` task), `server_task_result_slot_save_load`, `server_task_result_slot_erase`, `server_task_result_apply_lora`, `server_task_result_get_lora`, `server_task_result_error`, `format_error_response`, `server_task::need_sampling()`, `server_task::n_tokens()`, `server_schema::eval_llama_cmpl_schema()` (parsing pipeline + grammar routing + error paths + per-request `dry_*` and `sse_ping_interval` field round-trips incl. hard-limit + server-default inheritance), `response_fields` projection |
| `src/test/cpp/test_json_helpers.cpp` | 67 | All functions in `json_helpers.hpp`: `get_result_error_message`, `results_to_json`, `rerank_results_to_json` (incl. missing/out-of-range `index` rejection), `parse_encoding_format`, `extract_embedding_prompt`, `is_infill_request`, `parse_slot_prompt_similarity`, `parse_positive_int_config`, `wrap_stream_chunk`, `server_metrics_to_json`, `route_error_message` |
| `src/test/cpp/test_json_helpers.cpp` | 71 | All functions in `json_helpers.hpp`: `get_result_error_message`, `get_result_error_code` (the HTTP code of a failed task result, from which `jllama.cpp` picks `InvalidRequestException` or `LlamaException`), `results_to_json`, `rerank_results_to_json` (incl. missing/out-of-range `index` rejection), `parse_encoding_format`, `extract_embedding_prompt`, `is_infill_request`, `parse_slot_prompt_similarity`, `parse_positive_int_config`, `wrap_stream_chunk`, `server_metrics_to_json`, `route_error_message` |
| `src/test/cpp/test_log_helpers.cpp` | 13 | All functions in `log_helpers.hpp`: `log_level_name`, `format_log_as_json` |
| `src/test/cpp/test_common_log_callback.cpp` | 6 | **The runnable guard for `patches/0014`**: `common_log_set_callback()` on a private `common_log_init()` instance (never `common_log_main()`, so the process-wide logger the other tests print through is untouched) — delivery of level + bare text, no prefix/timestamp even when both are on (what `common_init()` does), clearing stops delivery, a swap drains queued entries to the *previous* sink (the property behind `LlamaModel.setLogger(format, null)` being a synchronous flush), a `--log-file` keeps being written alongside the sink, and every `ggml_log_level` passes through unchanged. The Java half (`LlamaLoggerTest`, model-free) proves the JNI trampoline on top of it. |
| `src/test/cpp/test_jni_helpers.cpp` | 70 | All functions in `jni_helpers.hpp` using a zero-filled `JNINativeInterface_` mock (incl. the `utf8_to_jstring_impl` byte-array string path: emoji byte-preservation, truncated-UTF-8 replace-not-throw). Seven of them pin `jni_guard_impl` — the JNI exception boundary every `Java_*` entry point runs inside — including the `catch (...)` arm that is the only backstop for a non-`std::exception` type, and its two refusals (never `ThrowNew` over a pending Java exception, never with a null class). |
Expand All @@ -2163,11 +2182,11 @@ ctest --test-dir build --output-on-failure -R "ResultsToJson"

| `src/test/cpp/test_native_server_shutdown.cpp` | 3 | **The runnable guard for the shutdown-handler guard in `patches/0006`/`0007`.** Runs the real `llama_server()` in router mode over an empty `--models-dir` on an ephemeral loopback port (no model, no worker) and stops it the way `native_server.cpp` does. Pins a clean stop (exit code 0), that `llama_server_request_shutdown()` **after** the server returned is a no-op (the deterministic form of the CI `SIGSEGV` — it crashed every run before the fix), and that requests hammered from another thread while the server tears down are safe (5 rounds). Compiled on non-Android only, like `server.cpp` itself. |

**Current total: 603 tests (all passing).**
**Current total: 607 tests (all passing).**

#### Upstream source location (in CMake build tree)

llama.cpp is fetched via CMake FetchContent, pinned to `GIT_TAG b11534`.
llama.cpp is fetched via CMake FetchContent, pinned to `GIT_TAG b11538`.

**GoogleTest** is a separate `BUILD_TESTING`-only FetchContent (`GIT_TAG v1.18.0`), used solely
by the `jllama_test` C++ unit-test binary — not by the shipped library, and not coupled to the
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
**Build:**
![Java 8+](https://img.shields.io/badge/Java-8%2B-informational)
![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows%20%7C%20Android-lightgrey)
[![llama.cpp b11534](https://img.shields.io/badge/llama.cpp-%23b11534-informational)](https://github.com/ggml-org/llama.cpp/releases/tag/b11534)
[![llama.cpp b11538](https://img.shields.io/badge/llama.cpp-%23b11538-informational)](https://github.com/ggml-org/llama.cpp/releases/tag/b11538)
[![JPMS](https://img.shields.io/badge/JPMS-modular%20JAR-25A162)](https://openjdk.org/projects/jigsaw/)
![JUnit](https://img.shields.io/badge/tested%20with-JUnit6-25A162)
[![JSpecify](https://img.shields.io/badge/JSpecify-1.0.0%20%40NullMarked-25A162)](https://jspecify.dev)
Expand Down
Loading
Loading